diff --git a/docs/03-resources/10-global-search.md b/docs/03-resources/10-global-search.md index be29b6bea9..d58ed685a6 100644 --- a/docs/03-resources/10-global-search.md +++ b/docs/03-resources/10-global-search.md @@ -172,6 +172,29 @@ public function panel(Panel $panel): Panel } ``` +## Requiring resources to opt in to global search + +By default, all resources with a [title attribute](#setting-global-search-result-titles) are included in global search results. If you'd prefer resources to explicitly opt in, you can use the `globalSearchResourceOptIn()` method in the [configuration](../panel-configuration): + +```php +use Filament\Panel; + +public function panel(Panel $panel): Panel +{ + return $panel + // ... + ->globalSearchResourceOptIn(); +} +``` + +Now, only resources that explicitly set `$isGloballySearchable` to `true` will be included in global search results: + +```php +protected static bool $isGloballySearchable = true; +``` + +Resources that do not declare this property will be excluded from global search, even if they have a title attribute set. + ## Registering global search key bindings The global search field can be opened using keyboard shortcuts. To configure these, pass the `globalSearchKeyBindings()` method to the [configuration](../panel-configuration): diff --git a/packages/panels/src/Panel/Concerns/HasGlobalSearch.php b/packages/panels/src/Panel/Concerns/HasGlobalSearch.php index 539eca7ca5..8dbff3945c 100644 --- a/packages/panels/src/Panel/Concerns/HasGlobalSearch.php +++ b/packages/panels/src/Panel/Concerns/HasGlobalSearch.php @@ -26,6 +26,8 @@ trait HasGlobalSearch protected string | Closure | null $globalSearchFieldSuffix = null; + protected bool $isGlobalSearchResourceOptIn = false; + public function globalSearch(string | bool $provider = true, GlobalSearchPosition | Closure | null $position = null): static { if (is_string($provider) && (! in_array(GlobalSearchProvider::class, class_implements($provider)))) { @@ -121,6 +123,18 @@ trait HasGlobalSearch return $this->evaluate($this->globalSearchFieldSuffix); } + public function globalSearchResourceOptIn(bool $condition = true): static + { + $this->isGlobalSearchResourceOptIn = $condition; + + return $this; + } + + public function isGlobalSearchResourceOptIn(): bool + { + return $this->isGlobalSearchResourceOptIn; + } + public function getGlobalSearchProvider(): ?GlobalSearchProvider { $provider = $this->globalSearchProvider; diff --git a/packages/panels/src/Resources/Resource/Concerns/HasGlobalSearch.php b/packages/panels/src/Resources/Resource/Concerns/HasGlobalSearch.php index 434934a0ff..aaa70dc08b 100644 --- a/packages/panels/src/Resources/Resource/Concerns/HasGlobalSearch.php +++ b/packages/panels/src/Resources/Resource/Concerns/HasGlobalSearch.php @@ -3,6 +3,7 @@ namespace Filament\Resources\Resource\Concerns; use Filament\Actions\Action; +use Filament\Facades\Filament; use Filament\GlobalSearch\GlobalSearchResult; use Illuminate\Contracts\Support\Htmlable; use Illuminate\Database\Connection; @@ -10,6 +11,7 @@ use Illuminate\Database\Eloquent\Builder; use Illuminate\Database\Eloquent\Model; use Illuminate\Support\Arr; use Illuminate\Support\Collection; +use ReflectionProperty; use function Filament\Support\generate_search_column_expression; use function Filament\Support\generate_search_term_expression; @@ -31,7 +33,18 @@ trait HasGlobalSearch public static function canGloballySearch(): bool { - return static::$isGloballySearchable && count(static::getGloballySearchableAttributes()) && static::canAccess(); + $isGloballySearchable = static::$isGloballySearchable; + + if ( + $isGloballySearchable && + Filament::getCurrentOrDefaultPanel()?->isGlobalSearchResourceOptIn() + ) { + $isGloballySearchable = (new ReflectionProperty(static::class, 'isGloballySearchable')) + ->getDeclaringClass() + ->getName() === static::class; + } + + return $isGloballySearchable && count(static::getGloballySearchableAttributes()) && static::canAccess(); } /** diff --git a/tests/src/Panels/GlobalSearch/GlobalSearchTest.php b/tests/src/Panels/GlobalSearch/GlobalSearchTest.php index 78dd3ec9d7..47b558914e 100644 --- a/tests/src/Panels/GlobalSearch/GlobalSearchTest.php +++ b/tests/src/Panels/GlobalSearch/GlobalSearchTest.php @@ -5,8 +5,11 @@ use Filament\GlobalSearch\GlobalSearchResult; use Filament\GlobalSearch\GlobalSearchResults; use Filament\GlobalSearch\Providers\Contracts\GlobalSearchProvider; use Filament\Livewire\GlobalSearch; +use Filament\Resources\Resource; use Filament\Tests\Fixtures\Models\Post; use Filament\Tests\Fixtures\Models\User; +use Filament\Tests\Fixtures\Resources\Posts\PostResource; +use Filament\Tests\Fixtures\Resources\Users\UserResource; use Filament\Tests\Panels\GlobalSearch\TestCase; use Illuminate\Database\Eloquent\Factories\Sequence; use Illuminate\Support\Str; @@ -78,6 +81,54 @@ it('orders resource global search results by `$globalSearchSort`', function (): expect($categories[1])->toBe('posts'); }); +it('excludes resources without explicit `$isGloballySearchable` when `globalSearchResourceOptIn()` is enabled', function (): void { + Filament::getCurrentOrDefaultPanel()->globalSearchResourceOptIn(); + + expect(PostResource::canGloballySearch())->toBeFalse(); + expect(UserResource::canGloballySearch())->toBeFalse(); +}); + +it('includes resources with explicit `$isGloballySearchable` when `globalSearchResourceOptIn()` is enabled', function (): void { + Filament::getCurrentOrDefaultPanel()->globalSearchResourceOptIn(); + + expect(OptedInGlobalSearchResource::canGloballySearch())->toBeTrue(); +}); + +it('includes all resources by default without `globalSearchResourceOptIn()`', function (): void { + expect(PostResource::canGloballySearch())->toBeTrue(); + expect(UserResource::canGloballySearch())->toBeTrue(); + expect(OptedInGlobalSearchResource::canGloballySearch())->toBeTrue(); +}); + +it('does not return search results for resources without explicit `$isGloballySearchable` when `globalSearchResourceOptIn()` is enabled', function (): void { + Filament::getCurrentOrDefaultPanel()->globalSearchResourceOptIn(); + + $post = Post::factory()->create(); + + livewire(GlobalSearch::class) + ->set('search', $post->title) + ->assertDontSee($post->title); +}); + +class OptedInGlobalSearchResource extends Resource +{ + protected static ?string $model = Post::class; + + protected static ?string $recordTitleAttribute = 'title'; + + protected static bool $isGloballySearchable = true; + + protected static ?string $slug = 'opted-in-global-search-posts'; + + /** + * @return array + */ + public static function getGloballySearchableAttributes(): array + { + return ['title']; + } +} + class CustomSearchProvider implements GlobalSearchProvider { public function getResults(string $query): ?GlobalSearchResults