diff --git a/packages/tables/docs/02-columns/01-overview.md b/packages/tables/docs/02-columns/01-overview.md index 57f2f799ddf..7373b758c48 100644 --- a/packages/tables/docs/02-columns/01-overview.md +++ b/packages/tables/docs/02-columns/01-overview.md @@ -542,6 +542,20 @@ public function table(Table $table): Table } ``` +You can also use the `splitIndividualSearchTerms()` method on a column to override the table's setting for that column's [individual search](#searching-individually): + +```php +use Filament\Tables\Columns\TextColumn; + +TextColumn::make('title') + ->searchable(isIndividual: true) + ->splitIndividualSearchTerms(false) +``` + + + ## Clickable cell content When a cell is clicked, you may open a URL or trigger an "action". diff --git a/packages/tables/src/Columns/Concerns/CanBeSearchable.php b/packages/tables/src/Columns/Concerns/CanBeSearchable.php index 884b627e409..ae8d311e2e3 100644 --- a/packages/tables/src/Columns/Concerns/CanBeSearchable.php +++ b/packages/tables/src/Columns/Concerns/CanBeSearchable.php @@ -23,6 +23,8 @@ trait CanBeSearchable protected bool | Closure | null $isSearchForcedCaseInsensitive = null; + protected bool | Closure | null $shouldSplitIndividualSearchTerms = null; + /** * @param bool | array | string | Closure $condition */ @@ -54,6 +56,13 @@ public function forceSearchCaseInsensitive(bool | Closure | null $condition = tr return $this; } + public function splitIndividualSearchTerms(bool | Closure | null $condition = true): static + { + $this->shouldSplitIndividualSearchTerms = $condition; + + return $this; + } + /** * @return array */ @@ -82,6 +91,11 @@ public function isSearchForcedCaseInsensitive(): ?bool return $this->evaluate($this->isSearchForcedCaseInsensitive); } + public function shouldSplitIndividualSearchTerms(): ?bool + { + return $this->evaluate($this->shouldSplitIndividualSearchTerms); + } + /** * @return array{0: string} */ diff --git a/packages/tables/src/Concerns/CanSearchRecords.php b/packages/tables/src/Concerns/CanSearchRecords.php index 21a451e7e05..d9a1db03a1c 100644 --- a/packages/tables/src/Concerns/CanSearchRecords.php +++ b/packages/tables/src/Concerns/CanSearchRecords.php @@ -75,7 +75,7 @@ protected function applySearchToTableQuery(Builder $query): Builder protected function applyColumnSearchesToTableQuery(Builder $query): Builder { $table = $this->getTable(); - $shouldSplitSearchTerms = $table->shouldSplitSearchTerms(); + $shouldSplitSearchTermsByDefault = $table->shouldSplitSearchTerms(); foreach ($this->getTableColumnSearches() as $column => $search) { if (blank($search)) { @@ -96,7 +96,7 @@ protected function applyColumnSearchesToTableQuery(Builder $query): Builder continue; } - if (! $shouldSplitSearchTerms) { + if (! ($column->shouldSplitIndividualSearchTerms() ?? $shouldSplitSearchTermsByDefault)) { $isFirst = true; $column->applySearchConstraint( diff --git a/tests/src/Fixtures/Livewire/PostsTableWithColumnIndividualSearchTermSplittingDisabled.php b/tests/src/Fixtures/Livewire/PostsTableWithColumnIndividualSearchTermSplittingDisabled.php new file mode 100644 index 00000000000..4da9fcfdcdf --- /dev/null +++ b/tests/src/Fixtures/Livewire/PostsTableWithColumnIndividualSearchTermSplittingDisabled.php @@ -0,0 +1,40 @@ +query(Post::query()) + ->columns([ + TextColumn::make('title') + ->searchable(isIndividual: true, isGlobal: false), + TextColumn::make('content') + ->searchable(isIndividual: true, isGlobal: false) + ->splitIndividualSearchTerms(false), + ]); + } + + public function render(): View + { + return view('livewire.table'); + } +} diff --git a/tests/src/Fixtures/Livewire/PostsTableWithColumnIndividualSearchTermSplittingEnabled.php b/tests/src/Fixtures/Livewire/PostsTableWithColumnIndividualSearchTermSplittingEnabled.php new file mode 100644 index 00000000000..8ff192a5963 --- /dev/null +++ b/tests/src/Fixtures/Livewire/PostsTableWithColumnIndividualSearchTermSplittingEnabled.php @@ -0,0 +1,41 @@ +query(Post::query()) + ->splitSearchTerms(false) + ->columns([ + TextColumn::make('title') + ->searchable(isIndividual: true, isGlobal: false), + TextColumn::make('content') + ->searchable(isIndividual: true, isGlobal: false) + ->splitIndividualSearchTerms(), + ]); + } + + public function render(): View + { + return view('livewire.table'); + } +} diff --git a/tests/src/Tables/ColumnTest.php b/tests/src/Tables/ColumnTest.php index 8dd688a45f7..66e689b4fd6 100644 --- a/tests/src/Tables/ColumnTest.php +++ b/tests/src/Tables/ColumnTest.php @@ -4,6 +4,8 @@ use Filament\Tables\Columns\TextColumn; use Filament\Tests\Fixtures\Livewire\CustomDataTable; use Filament\Tests\Fixtures\Livewire\PostsTable; +use Filament\Tests\Fixtures\Livewire\PostsTableWithColumnIndividualSearchTermSplittingDisabled; +use Filament\Tests\Fixtures\Livewire\PostsTableWithColumnIndividualSearchTermSplittingEnabled; use Filament\Tests\Fixtures\Livewire\PostsTableWithQualifiedColumns; use Filament\Tests\Fixtures\Livewire\PostsTableWithReservedJsPropertyColumnSearch; use Filament\Tests\Fixtures\Livewire\PostsTableWithTableSearchableColumns; @@ -356,6 +358,44 @@ ->assertCanNotSeeTableRecords($posts->where('author.email', '!=', $authorEmail)); }); + it('can use `splitIndividualSearchTerms(false)` on a column to disable search term splitting for its individual search', function (): void { + $exactPhrasePost = Post::factory()->create(['content' => 'ipsum dolor']); + $separateWordsPost = Post::factory()->create(['content' => 'ipsum sit dolor']); + + livewire(PostsTableWithColumnIndividualSearchTermSplittingDisabled::class) + ->searchTableColumns(['content' => 'ipsum dolor']) + ->assertCanSeeTableRecords([$exactPhrasePost]) + ->assertCanNotSeeTableRecords([$separateWordsPost]); + }); + + it('splits search terms for an individual column search by default, when the column does not define `splitIndividualSearchTerms()`', function (): void { + $exactPhrasePost = Post::factory()->create(['title' => 'ipsum dolor']); + $separateWordsPost = Post::factory()->create(['title' => 'ipsum sit dolor']); + + livewire(PostsTableWithColumnIndividualSearchTermSplittingDisabled::class) + ->searchTableColumns(['title' => 'ipsum dolor']) + ->assertCanSeeTableRecords([$exactPhrasePost, $separateWordsPost]); + }); + + it('can use `splitIndividualSearchTerms()` on a column to enable search term splitting for its individual search when the table uses `splitSearchTerms(false)`', function (): void { + $exactPhrasePost = Post::factory()->create(['content' => 'ipsum dolor']); + $separateWordsPost = Post::factory()->create(['content' => 'ipsum sit dolor']); + + livewire(PostsTableWithColumnIndividualSearchTermSplittingEnabled::class) + ->searchTableColumns(['content' => 'ipsum dolor']) + ->assertCanSeeTableRecords([$exactPhrasePost, $separateWordsPost]); + }); + + it('does not split search terms for an individual column search when the table uses `splitSearchTerms(false)` and the column does not define `splitIndividualSearchTerms()`', function (): void { + $exactPhrasePost = Post::factory()->create(['title' => 'ipsum dolor']); + $separateWordsPost = Post::factory()->create(['title' => 'ipsum sit dolor']); + + livewire(PostsTableWithColumnIndividualSearchTermSplittingEnabled::class) + ->searchTableColumns(['title' => 'ipsum dolor']) + ->assertCanSeeTableRecords([$exactPhrasePost]) + ->assertCanNotSeeTableRecords([$separateWordsPost]); + }); + it('seeds individually searchable columns whose names collide with JS array properties on mount so `tableColumnSearches` serializes as a JSON object', function (): void { livewire(PostsTableWithReservedJsPropertyColumnSearch::class) ->assertSet('tableColumnSearches', ['length' => '', 'sort' => '']);