diff --git a/app/Filament/Resources/PluginResource.php b/app/Filament/Resources/PluginResource.php index 8e6abc0d..e1cec071 100644 --- a/app/Filament/Resources/PluginResource.php +++ b/app/Filament/Resources/PluginResource.php @@ -22,6 +22,7 @@ use Filament\Schemas; use Filament\Schemas\Schema; use Filament\Tables; +use Filament\Tables\Contracts\HasTable; use Filament\Tables\Table; use Illuminate\Database\Eloquent\Builder; use Illuminate\Support\HtmlString; @@ -259,17 +260,19 @@ public static function table(Table $table): Table return $table ->columns([ Tables\Columns\ImageColumn::make('logo_path') - ->label('') + ->label('Logo') ->disk('public') ->circular() ->defaultImageUrl(fn () => 'https://ui-avatars.com/api/?name=P&color=7C3AED&background=EDE9FE') - ->size(40), + ->size(40) + ->toggleable(), Tables\Columns\TextColumn::make('name') ->label('Package Name') ->searchable() ->sortable() - ->fontFamily('mono'), + ->fontFamily('mono') + ->toggleable(), Tables\Columns\TextColumn::make('type') ->badge() @@ -277,18 +280,21 @@ public static function table(Table $table): Table PluginType::Free => 'gray', PluginType::Paid => 'success', }) - ->sortable(), + ->sortable() + ->toggleable(), Tables\Columns\TextColumn::make('tier') ->badge() ->color(fn (?PluginTier $state): string => $state?->color() ?? 'gray') ->sortable() - ->placeholder('-'), + ->placeholder('-') + ->toggleable(), Tables\Columns\TextColumn::make('user.email') ->label('Submitted By') ->searchable() - ->sortable(), + ->sortable() + ->toggleable(), Tables\Columns\TextColumn::make('status') ->badge() @@ -298,7 +304,8 @@ public static function table(Table $table): Table PluginStatus::Approved => 'success', PluginStatus::Rejected => 'danger', }) - ->sortable(), + ->sortable() + ->toggleable(), Tables\Columns\TextColumn::make('mobile_min_version') ->label('Mobile SDK') @@ -307,25 +314,30 @@ public static function table(Table $table): Table Tables\Columns\ToggleColumn::make('is_official') ->label('Official') - ->sortable(), + ->sortable() + ->toggleable(), Tables\Columns\ToggleColumn::make('featured') - ->sortable(), + ->sortable() + ->toggleable(), Tables\Columns\ToggleColumn::make('is_active') ->label('Active') - ->sortable(), + ->sortable() + ->toggleable(), Tables\Columns\ToggleColumn::make('works_in_jump') ->label('Jump') - ->sortable(), + ->sortable() + ->toggleable(), Tables\Columns\IconColumn::make('reviewed_at') ->label('Reviewed') ->boolean() ->getStateUsing(fn (Plugin $record): bool => $record->reviewed_at !== null) ->tooltip(fn (Plugin $record): ?string => $record->reviewed_at?->diffForHumans()) - ->sortable(), + ->sortable() + ->toggleable(), Tables\Columns\IconColumn::make('satis_synced_at') ->label('Satis') @@ -333,25 +345,36 @@ public static function table(Table $table): Table ->getStateUsing(fn (Plugin $record): bool => $record->isSatisSynced()) ->tooltip(fn (Plugin $record): ?string => $record->satis_synced_at?->diffForHumans()) ->visible(fn (): bool => true) - ->sortable(), + ->sortable() + ->toggleable(), Tables\Columns\TextColumn::make('created_at') ->label('Submitted') ->dateTime() - ->sortable(), + ->sortable() + ->toggleable(), Tables\Columns\TextColumn::make('approved_at') ->label('Approved') ->dateTime() ->placeholder('-') - ->sortable(), + ->sortable() + ->toggleable(), ]) + ->reorderableColumns() ->filters([ - Tables\Filters\SelectFilter::make('status') - ->options(PluginStatus::class) - ->query(fn (Builder $query, array $data): Builder => filled($data['value']) - ? $query->where('status', $data['value']) + Tables\Filters\Filter::make('drafts') + ->schema([ + Forms\Components\Checkbox::make('show_drafts') + ->label('Show drafts'), + ]) + ->query(fn (Builder $query, array $data): Builder => ($data['show_drafts'] ?? false) + ? $query : $query->where('status', '!=', PluginStatus::Draft) + ) + ->indicateUsing(fn (array $data): array => ($data['show_drafts'] ?? false) + ? ['show_drafts' => 'Showing drafts'] + : [] ), Tables\Filters\SelectFilter::make('type') ->options(PluginType::class), @@ -507,7 +530,12 @@ public static function table(Table $table): Table ->bulkActions([ ]) - ->defaultSort('created_at', 'desc'); + ->defaultSort( + fn (HasTable $livewire): string => ($livewire instanceof Pages\ListPlugins && $livewire->getActiveTabStatus() === PluginStatus::Approved) + ? 'approved_at' + : 'created_at', + 'desc', + ); } public static function getRelations(): array diff --git a/app/Filament/Resources/PluginResource/Pages/ListPlugins.php b/app/Filament/Resources/PluginResource/Pages/ListPlugins.php index ba97676c..c22ac682 100644 --- a/app/Filament/Resources/PluginResource/Pages/ListPlugins.php +++ b/app/Filament/Resources/PluginResource/Pages/ListPlugins.php @@ -2,9 +2,12 @@ namespace App\Filament\Resources\PluginResource\Pages; +use App\Enums\PluginStatus; use App\Filament\Resources\PluginResource; use Filament\Actions; use Filament\Resources\Pages\ListRecords; +use Filament\Schemas\Components\Tabs\Tab; +use Illuminate\Database\Eloquent\Builder; class ListPlugins extends ListRecords { @@ -16,4 +19,28 @@ protected function getHeaderActions(): array Actions\CreateAction::make(), ]; } + + public function getTabs(): array + { + return [ + PluginStatus::Pending->value => $this->makeStatusTab(PluginStatus::Pending, 'Pending'), + PluginStatus::Approved->value => $this->makeStatusTab(PluginStatus::Approved, 'Approved'), + PluginStatus::Rejected->value => $this->makeStatusTab(PluginStatus::Rejected, 'Rejected'), + 'all' => Tab::make('All'), + ]; + } + + /** + * The status the active tab is scoped to, or null when the tab lists every status. + */ + public function getActiveTabStatus(): ?PluginStatus + { + return PluginStatus::tryFrom($this->activeTab ?? ''); + } + + private function makeStatusTab(PluginStatus $status, string $label): Tab + { + return Tab::make($label) + ->modifyQueryUsing(fn (Builder $query) => $query->where('status', $status)); + } } diff --git a/tests/Feature/Filament/GrantPluginToUserActionTest.php b/tests/Feature/Filament/GrantPluginToUserActionTest.php index 4dd650b6..f191de61 100644 --- a/tests/Feature/Filament/GrantPluginToUserActionTest.php +++ b/tests/Feature/Filament/GrantPluginToUserActionTest.php @@ -31,6 +31,7 @@ public function test_grant_to_user_action_is_hidden_for_free_plugins_on_list(): Livewire::actingAs($this->admin) ->test(ListPlugins::class) + ->set('activeTab', 'approved') ->assertTableActionHidden('grantToUser', $plugin); } @@ -40,6 +41,7 @@ public function test_grant_to_user_action_is_visible_for_paid_plugins_on_list(): Livewire::actingAs($this->admin) ->test(ListPlugins::class) + ->set('activeTab', 'approved') ->assertTableActionVisible('grantToUser', $plugin); } @@ -68,6 +70,7 @@ public function test_grant_to_user_action_can_be_called_with_user_id_on_list(): Livewire::actingAs($this->admin) ->test(ListPlugins::class) + ->set('activeTab', 'approved') ->callAction( TestAction::make('grantToUser')->table($plugin), data: ['user_id' => $recipient->id], diff --git a/tests/Feature/Filament/PluginListDraftFilterTest.php b/tests/Feature/Filament/PluginListDraftFilterTest.php index acb5cc10..a1e087bb 100644 --- a/tests/Feature/Filament/PluginListDraftFilterTest.php +++ b/tests/Feature/Filament/PluginListDraftFilterTest.php @@ -2,7 +2,6 @@ namespace Tests\Feature\Filament; -use App\Enums\PluginStatus; use App\Filament\Resources\PluginResource\Pages\ListPlugins; use App\Models\Plugin; use App\Models\User; @@ -24,6 +23,18 @@ protected function setUp(): void config(['filament.users' => ['admin@test.com']]); } + public function test_show_drafts_checkbox_renders_and_no_status_filter_remains(): void + { + Plugin::factory()->pending()->create(); + + $component = Livewire::actingAs($this->admin) + ->test(ListPlugins::class) + ->assertTableFilterExists('drafts') + ->assertSee('Show drafts'); + + $this->assertNull($component->instance()->getTable()->getFilter('status')); + } + public function test_draft_plugins_are_hidden_by_default(): void { $draft = Plugin::factory()->draft()->create(); @@ -31,19 +42,47 @@ public function test_draft_plugins_are_hidden_by_default(): void Livewire::actingAs($this->admin) ->test(ListPlugins::class) + ->set('activeTab', 'all') ->assertCanNotSeeTableRecords([$draft]) ->assertCanSeeTableRecords([$approved]); } - public function test_draft_plugins_are_visible_when_filtering_by_draft_status(): void + public function test_draft_plugins_are_visible_when_show_drafts_is_checked(): void { $draft = Plugin::factory()->draft()->create(); $approved = Plugin::factory()->approved()->create(); Livewire::actingAs($this->admin) ->test(ListPlugins::class) - ->filterTable('status', PluginStatus::Draft->value) + ->set('activeTab', 'all') + ->filterTable('drafts', ['show_drafts' => true]) + ->assertCanSeeTableRecords([$draft, $approved]); + } + + public function test_drafts_are_hidden_again_when_show_drafts_is_unchecked(): void + { + $draft = Plugin::factory()->draft()->create(); + $approved = Plugin::factory()->approved()->create(); + + Livewire::actingAs($this->admin) + ->test(ListPlugins::class) + ->set('activeTab', 'all') + ->filterTable('drafts', ['show_drafts' => true]) ->assertCanSeeTableRecords([$draft]) - ->assertCanNotSeeTableRecords([$approved]); + ->filterTable('drafts', ['show_drafts' => false]) + ->assertCanNotSeeTableRecords([$draft]) + ->assertCanSeeTableRecords([$approved]); + } + + public function test_show_drafts_does_not_leak_drafts_into_the_status_tabs(): void + { + $draft = Plugin::factory()->draft()->create(); + $pending = Plugin::factory()->pending()->create(); + + Livewire::actingAs($this->admin) + ->test(ListPlugins::class) + ->filterTable('drafts', ['show_drafts' => true]) + ->assertCanSeeTableRecords([$pending]) + ->assertCanNotSeeTableRecords([$draft]); } } diff --git a/tests/Feature/Filament/PluginListTabsTest.php b/tests/Feature/Filament/PluginListTabsTest.php new file mode 100644 index 00000000..472d13f9 --- /dev/null +++ b/tests/Feature/Filament/PluginListTabsTest.php @@ -0,0 +1,209 @@ +admin = User::factory()->create(['email' => 'admin@test.com']); + config(['filament.users' => ['admin@test.com']]); + } + + public function test_pending_is_the_default_tab(): void + { + $pending = Plugin::factory()->pending()->create(); + $approved = Plugin::factory()->approved()->create(); + $rejected = Plugin::factory()->rejected()->create(); + $draft = Plugin::factory()->draft()->create(); + + Livewire::actingAs($this->admin) + ->test(ListPlugins::class) + ->assertSet('activeTab', 'pending') + ->assertCanSeeTableRecords([$pending]) + ->assertCanNotSeeTableRecords([$approved, $rejected, $draft]); + } + + public function test_tabs_are_ordered_pending_approved_rejected_then_all(): void + { + $tabs = Livewire::actingAs($this->admin) + ->test(ListPlugins::class) + ->instance() + ->getTabs(); + + $this->assertSame(['pending', 'approved', 'rejected', 'all'], array_keys($tabs)); + } + + public function test_list_page_renders_the_status_tabs(): void + { + Plugin::factory()->pending()->create(); + + $response = $this->actingAs($this->admin)->get(PluginResource::getUrl('index')); + + $response->assertOk(); + $response->assertSeeInOrder(['Pending', 'Approved', 'Rejected', 'All']); + } + + public function test_approved_tab_only_shows_approved_plugins(): void + { + $pending = Plugin::factory()->pending()->create(); + $approved = Plugin::factory()->approved()->create(); + $rejected = Plugin::factory()->rejected()->create(); + + Livewire::actingAs($this->admin) + ->test(ListPlugins::class) + ->set('activeTab', 'approved') + ->assertCanSeeTableRecords([$approved]) + ->assertCanNotSeeTableRecords([$pending, $rejected]); + } + + public function test_approved_tab_sorts_by_approval_date_in_reverse_chronological_order(): void + { + $oldest = Plugin::factory()->approved()->create([ + 'created_at' => now()->subDays(30), + 'approved_at' => now()->subDays(10), + ]); + $newest = Plugin::factory()->approved()->create([ + 'created_at' => now()->subDays(2), + 'approved_at' => now()->subDay(), + ]); + $middle = Plugin::factory()->approved()->create([ + 'created_at' => now()->subDays(20), + 'approved_at' => now()->subDays(5), + ]); + + Livewire::actingAs($this->admin) + ->test(ListPlugins::class) + ->set('activeTab', 'approved') + ->assertCanSeeTableRecords([$newest, $middle, $oldest], inOrder: true); + } + + public function test_other_tabs_sort_by_submission_date_in_reverse_chronological_order(): void + { + $oldest = Plugin::factory()->pending()->create(['created_at' => now()->subDays(10)]); + $newest = Plugin::factory()->pending()->create(['created_at' => now()->subDay()]); + $middle = Plugin::factory()->pending()->create(['created_at' => now()->subDays(5)]); + + Livewire::actingAs($this->admin) + ->test(ListPlugins::class) + ->assertCanSeeTableRecords([$newest, $middle, $oldest], inOrder: true); + } + + public function test_approved_tab_still_honours_a_column_sort_chosen_by_the_admin(): void + { + $first = Plugin::factory()->approved()->create([ + 'name' => 'acme/aaa-plugin', + 'approved_at' => now()->subDay(), + ]); + $last = Plugin::factory()->approved()->create([ + 'name' => 'acme/zzz-plugin', + 'approved_at' => now()->subDays(10), + ]); + + Livewire::actingAs($this->admin) + ->test(ListPlugins::class) + ->set('activeTab', 'approved') + ->sortTable('name', 'asc') + ->assertCanSeeTableRecords([$first, $last], inOrder: true) + ->sortTable('name', 'desc') + ->assertCanSeeTableRecords([$last, $first], inOrder: true); + } + + public function test_rejected_tab_only_shows_rejected_plugins(): void + { + $pending = Plugin::factory()->pending()->create(); + $approved = Plugin::factory()->approved()->create(); + $rejected = Plugin::factory()->rejected()->create(); + + Livewire::actingAs($this->admin) + ->test(ListPlugins::class) + ->set('activeTab', 'rejected') + ->assertCanSeeTableRecords([$rejected]) + ->assertCanNotSeeTableRecords([$pending, $approved]); + } + + public function test_all_tab_shows_every_reviewable_status(): void + { + $pending = Plugin::factory()->pending()->create(); + $approved = Plugin::factory()->approved()->create(); + $rejected = Plugin::factory()->rejected()->create(); + + Livewire::actingAs($this->admin) + ->test(ListPlugins::class) + ->set('activeTab', 'all') + ->assertCanSeeTableRecords([$pending, $approved, $rejected]); + } + + public function test_hidden_columns_stay_hidden_when_switching_tabs(): void + { + Plugin::factory()->pending()->create(); + Plugin::factory()->approved()->create(); + + $component = Livewire::actingAs($this->admin) + ->test(ListPlugins::class) + ->assertCanRenderTableColumn('featured'); + + $component + ->call('applyTableColumnManager', $this->hideColumn($component->get('tableColumns'), 'featured')) + ->assertCanNotRenderTableColumn('featured') + ->set('activeTab', 'approved') + ->assertCanNotRenderTableColumn('featured') + ->set('activeTab', 'all') + ->assertCanNotRenderTableColumn('featured'); + } + + public function test_hidden_columns_are_remembered_on_a_later_visit(): void + { + Plugin::factory()->pending()->create(); + + $component = Livewire::actingAs($this->admin)->test(ListPlugins::class); + + $component->call('applyTableColumnManager', $this->hideColumn($component->get('tableColumns'), 'featured')); + + Livewire::actingAs($this->admin) + ->test(ListPlugins::class) + ->assertCanNotRenderTableColumn('featured') + ->assertCanRenderTableColumn('name'); + } + + public function test_column_customisations_can_be_reset(): void + { + Plugin::factory()->pending()->create(); + + $component = Livewire::actingAs($this->admin)->test(ListPlugins::class); + + $component + ->call('applyTableColumnManager', $this->hideColumn($component->get('tableColumns'), 'featured')) + ->assertCanNotRenderTableColumn('featured') + ->call('resetTableColumnManager') + ->assertCanRenderTableColumn('featured'); + } + + /** + * @param array> $columns + * @return array> + */ + private function hideColumn(array $columns, string $name): array + { + return array_map( + fn (array $column): array => $column['name'] === $name + ? [...$column, 'isToggled' => false] + : $column, + $columns, + ); + } +} diff --git a/tests/Feature/Filament/PluginResourceTest.php b/tests/Feature/Filament/PluginResourceTest.php index 514efbbe..df9aa858 100644 --- a/tests/Feature/Filament/PluginResourceTest.php +++ b/tests/Feature/Filament/PluginResourceTest.php @@ -138,6 +138,7 @@ public function test_plugins_table_can_be_sorted_by_approved_at(): void Livewire::actingAs($this->admin) ->test(ListPlugins::class) + ->set('activeTab', 'all') ->assertCanRenderTableColumn('approved_at') ->sortTable('approved_at', 'desc') ->assertCanSeeTableRecords([$newest, $middle, $oldest], inOrder: true)