From 2988f104d1472b8b51d1de27a8a4cb6cc0357de6 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 19 Nov 2025 14:24:31 +0000 Subject: [PATCH 01/13] Rename `'searchables' => 'all'` to `content` --- config/search.php | 2 +- src/Search/Searchables.php | 2 +- tests/Search/Searchables/AssetsTest.php | 16 ++++++++-------- tests/Search/Searchables/EntriesTest.php | 16 ++++++++-------- tests/Search/Searchables/TermsTest.php | 12 ++++++------ tests/Search/Searchables/UsersTest.php | 16 ++++++++-------- tests/Search/SearchablesTest.php | 8 ++++---- 7 files changed, 36 insertions(+), 36 deletions(-) diff --git a/config/search.php b/config/search.php index ee4d6bd98dd..7c01ac882f2 100644 --- a/config/search.php +++ b/config/search.php @@ -27,7 +27,7 @@ 'default' => [ 'driver' => 'local', - 'searchables' => 'all', + 'searchables' => 'content', 'fields' => ['title'], ], diff --git a/src/Search/Searchables.php b/src/Search/Searchables.php index 8bdb1d6e0c6..43499e3fb92 100644 --- a/src/Search/Searchables.php +++ b/src/Search/Searchables.php @@ -29,7 +29,7 @@ private function makeProviders() $providers = collect(Arr::wrap($this->index->config()['searchables'] ?? [])); - if ($providers->contains('all')) { + if ($providers->contains('content')) { return $manager->providers()->map(fn ($_, $key) => $manager->make($key, $this->index, ['*'])); } diff --git a/tests/Search/Searchables/AssetsTest.php b/tests/Search/Searchables/AssetsTest.php index da1902bab7d..b8fb8db2df3 100644 --- a/tests/Search/Searchables/AssetsTest.php +++ b/tests/Search/Searchables/AssetsTest.php @@ -56,9 +56,9 @@ public function it_gets_assets($locale, $config, $expected) public static function assetsProvider() { return [ - 'all' => [ + 'content' => [ null, - ['searchables' => 'all'], + ['searchables' => 'content'], ['a', 'b', 'y', 'z'], ], 'all containers' => [ @@ -77,9 +77,9 @@ public static function assetsProvider() ['y', 'z'], ], - 'all, english' => [ + 'content, english' => [ 'en', - ['searchables' => 'all'], + ['searchables' => 'content'], ['a', 'b', 'y', 'z'], ], 'all containers, english' => [ @@ -98,9 +98,9 @@ public static function assetsProvider() ['y', 'z'], ], - 'all, french' => [ + 'content, french' => [ 'fr', - ['searchables' => 'all'], + ['searchables' => 'content'], ['a', 'b', 'y', 'z'], ], 'all containers, french' => [ @@ -138,7 +138,7 @@ public function it_can_use_a_custom_filter($filter) $d = tap(Asset::make()->container('images')->path('d.jpg'))->save(); $provider = $this->makeProvider(null, [ - 'searchables' => 'all', + 'searchables' => 'content', 'filter' => $filter, ]); @@ -185,7 +185,7 @@ private function normalizeSearchableKeys($keys) { // a bit of duplicated implementation logic. // but it makes the test look more like the real thing. - return collect($keys === 'all' ? ['*'] : $keys) + return collect($keys === 'content' ? ['*'] : $keys) ->map(fn ($key) => str_replace('assets:', '', $key)) ->all(); } diff --git a/tests/Search/Searchables/EntriesTest.php b/tests/Search/Searchables/EntriesTest.php index 003bff7c2c1..e2bddd86bf3 100644 --- a/tests/Search/Searchables/EntriesTest.php +++ b/tests/Search/Searchables/EntriesTest.php @@ -56,9 +56,9 @@ public function it_gets_entries($locale, $config, $expected) public static function entriesProvider() { return [ - 'all' => [ + 'content' => [ null, - ['searchables' => 'all'], + ['searchables' => 'content'], ['alfa', 'charlie', 'delta', 'foxtrot', 'xray', 'zulu'], ], 'all collections' => [ @@ -77,9 +77,9 @@ public static function entriesProvider() ['xray', 'zulu'], ], - 'all, english' => [ + 'content, english' => [ 'en', - ['searchables' => 'all'], + ['searchables' => 'content'], ['alfa', 'charlie', 'xray', 'zulu'], ], 'all collections, english' => [ @@ -98,9 +98,9 @@ public static function entriesProvider() ['xray', 'zulu'], ], - 'all, french' => [ + 'content, french' => [ 'fr', - ['searchables' => 'all'], + ['searchables' => 'content'], ['delta', 'foxtrot'], ], 'all collections, french' => [ @@ -133,7 +133,7 @@ public function it_can_use_a_custom_filter($filter) $e = EntryFactory::collection('blog')->slug('e')->create(); $provider = $this->makeProvider(null, [ - 'searchables' => 'all', + 'searchables' => 'content', 'filter' => $filter, ]); @@ -181,7 +181,7 @@ private function normalizeSearchableKeys($keys) { // a bit of duplicated implementation logic. // but it makes the test look more like the real thing. - return collect($keys === 'all' ? ['*'] : $keys) + return collect($keys === 'content' ? ['*'] : $keys) ->map(fn ($key) => str_replace('collection:', '', $key)) ->all(); } diff --git a/tests/Search/Searchables/TermsTest.php b/tests/Search/Searchables/TermsTest.php index 23b63cde015..fb73fd332f1 100644 --- a/tests/Search/Searchables/TermsTest.php +++ b/tests/Search/Searchables/TermsTest.php @@ -77,9 +77,9 @@ public function it_gets_terms($locale, $config, $expected) public static function termsProvider() { return [ - 'all' => [ + 'content' => [ null, - ['searchables' => 'all'], + ['searchables' => 'content'], [ 'term::tags::alfa::en', 'term::tags::alfa::fr', @@ -120,9 +120,9 @@ public static function termsProvider() ], ], - 'all, english' => [ + 'content, english' => [ 'en', - ['searchables' => 'all'], + ['searchables' => 'content'], [ 'term::tags::alfa::en', 'term::tags::bravo::en', @@ -200,7 +200,7 @@ public function it_can_use_a_custom_filter($filter) $d = tap(Term::make('d')->taxonomy('tags')->dataForLocale('en', []))->save(); $provider = $this->makeProvider(null, [ - 'searchables' => 'all', + 'searchables' => 'content', 'filter' => $filter, ]); @@ -247,7 +247,7 @@ private function normalizeSearchableKeys($keys) { // a bit of duplicated implementation logic. // but it makes the test look more like the real thing. - return collect($keys === 'all' ? ['*'] : $keys) + return collect($keys === 'content' ? ['*'] : $keys) ->map(fn ($key) => str_replace('taxonomy:', '', $key)) ->all(); } diff --git a/tests/Search/Searchables/UsersTest.php b/tests/Search/Searchables/UsersTest.php index 975cedbb183..5b1277ba906 100644 --- a/tests/Search/Searchables/UsersTest.php +++ b/tests/Search/Searchables/UsersTest.php @@ -43,9 +43,9 @@ public function it_gets_users($locale, $config, $expected) public static function usersProvider() { return [ - 'all' => [ + 'content' => [ null, - ['searchables' => 'all'], + ['searchables' => 'content'], ['alfa@test.com', 'bravo@test.com'], ], 'all users' => [ @@ -54,9 +54,9 @@ public static function usersProvider() ['alfa@test.com', 'bravo@test.com'], ], - 'all, english' => [ + 'content, english' => [ 'en', - ['searchables' => 'all'], + ['searchables' => 'content'], ['alfa@test.com', 'bravo@test.com'], ], 'all users, english' => [ @@ -65,9 +65,9 @@ public static function usersProvider() ['alfa@test.com', 'bravo@test.com'], ], - 'all, french' => [ + 'content, french' => [ 'fr', - ['searchables' => 'all'], + ['searchables' => 'content'], ['alfa@test.com', 'bravo@test.com'], ], 'all users, french' => [ @@ -88,7 +88,7 @@ public function it_can_use_a_custom_filter($filter) $d = tap(User::make()->email('d@test.com'))->save(); $provider = $this->makeProvider(null, [ - 'searchables' => 'all', + 'searchables' => 'content', 'filter' => $filter, ]); @@ -135,7 +135,7 @@ private function normalizeSearchableKeys($keys) { // a bit of duplicated implementation logic. // but it makes the test look more like the real thing. - return collect($keys === 'all' ? ['*'] : $keys) + return collect($keys === 'content' ? ['*'] : $keys) ->map(fn ($key) => str_replace('users:', '', $key)) ->all(); } diff --git a/tests/Search/SearchablesTest.php b/tests/Search/SearchablesTest.php index 63aab79d0db..a8f6e294e21 100644 --- a/tests/Search/SearchablesTest.php +++ b/tests/Search/SearchablesTest.php @@ -45,7 +45,7 @@ public function setUp(): void } #[Test] - public function it_checks_all_providers_for_whether_an_item_is_searchable() + public function it_checks_content_providers_for_whether_an_item_is_searchable() { app(Providers::class)->register($entries = Mockery::mock(Entries::class)->makePartial()); app(Providers::class)->register($terms = Mockery::mock(Terms::class)->makePartial()); @@ -53,7 +53,7 @@ public function it_checks_all_providers_for_whether_an_item_is_searchable() app(Providers::class)->register($users = Mockery::mock(Users::class)->makePartial()); $searchable = Mockery::mock(); - $searchables = $this->makeSearchables(['searchables' => 'all']); + $searchables = $this->makeSearchables(['searchables' => 'content']); // Check twice. // First time they'll all return false, so contains() will return false. @@ -69,7 +69,7 @@ public function it_checks_all_providers_for_whether_an_item_is_searchable() } #[Test] - public function all_searchables_include_entries_terms_assets_and_users() + public function content_searchables_include_entries_terms_assets_and_users() { app(Providers::class)->register($entries = Mockery::mock(Entries::class)->makePartial()); app(Providers::class)->register($terms = Mockery::mock(Terms::class)->makePartial()); @@ -81,7 +81,7 @@ public function all_searchables_include_entries_terms_assets_and_users() $assets->shouldReceive('provide')->andReturn(collect([$assetA = Asset::make(), $assetB = Asset::make()])); $users->shouldReceive('provide')->andReturn(collect([$userA = User::make(), $userB = User::make()])); - $searchables = $this->makeSearchables(['searchables' => 'all']); + $searchables = $this->makeSearchables(['searchables' => 'content']); $everything = [ $entryA, From 8da30219cf8ee83335814818cbea271a64939f8c Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 19 Nov 2025 14:44:18 +0000 Subject: [PATCH 02/13] `'searchables' => 'all'` should throw an exception --- src/Exceptions/AllSearchablesNotSupported.php | 8 ++++++++ src/Search/Searchables.php | 5 +++++ tests/Search/SearchablesTest.php | 9 +++++++++ 3 files changed, 22 insertions(+) create mode 100644 src/Exceptions/AllSearchablesNotSupported.php diff --git a/src/Exceptions/AllSearchablesNotSupported.php b/src/Exceptions/AllSearchablesNotSupported.php new file mode 100644 index 00000000000..cec9c94368a --- /dev/null +++ b/src/Exceptions/AllSearchablesNotSupported.php @@ -0,0 +1,8 @@ +index->config()['searchables'] ?? [])); + if ($providers->contains('all')) { + throw new AllSearchablesNotSupported("'searchables' => 'all' is no longer supported. Please see the upgrade guide for more information: https://statamic.dev/getting-started/upgrade-guide/5-to-6"); + } + if ($providers->contains('content')) { return $manager->providers()->map(fn ($_, $key) => $manager->make($key, $this->index, ['*'])); } diff --git a/tests/Search/SearchablesTest.php b/tests/Search/SearchablesTest.php index a8f6e294e21..770013f2baa 100644 --- a/tests/Search/SearchablesTest.php +++ b/tests/Search/SearchablesTest.php @@ -9,6 +9,7 @@ use PHPUnit\Framework\Assert; use PHPUnit\Framework\Attributes\Test; use Statamic\Contracts\Entries\Entry as EntryContract; +use Statamic\Exceptions\AllSearchablesNotSupported; use Statamic\Facades\Asset; use Statamic\Facades\AssetContainer; use Statamic\Facades\Entry; @@ -44,6 +45,14 @@ public function setUp(): void AssetContainer::make('audio')->disk('audio')->save(); } + #[Test] + public function it_throws_an_exception_when_all_searchables_are_configured() + { + $this->expectException(AllSearchablesNotSupported::class); + + $this->makeSearchables(['searchables' => 'all']); + } + #[Test] public function it_checks_content_providers_for_whether_an_item_is_searchable() { From eebd2befbc9080fcaa9548470448aff9b91b3616 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 19 Nov 2025 14:55:04 +0000 Subject: [PATCH 03/13] `'searchables' => 'content'` shouldn't include users searchable --- src/Search/Searchables.php | 4 +++- tests/Search/SearchablesTest.php | 28 ++++++++++++++++++++-------- 2 files changed, 23 insertions(+), 9 deletions(-) diff --git a/src/Search/Searchables.php b/src/Search/Searchables.php index 295c49d5985..b72dbb03fb0 100644 --- a/src/Search/Searchables.php +++ b/src/Search/Searchables.php @@ -35,7 +35,9 @@ private function makeProviders() } if ($providers->contains('content')) { - return $manager->providers()->map(fn ($_, $key) => $manager->make($key, $this->index, ['*'])); + return $manager->providers() + ->filter(fn ($_, $key) => in_array($key, ['collection', 'taxonomy', 'assets'])) + ->map(fn ($_, $key) => $manager->make($key, $this->index, ['*'])); } return $providers diff --git a/tests/Search/SearchablesTest.php b/tests/Search/SearchablesTest.php index 770013f2baa..b526034ae3b 100644 --- a/tests/Search/SearchablesTest.php +++ b/tests/Search/SearchablesTest.php @@ -59,36 +59,32 @@ public function it_checks_content_providers_for_whether_an_item_is_searchable() app(Providers::class)->register($entries = Mockery::mock(Entries::class)->makePartial()); app(Providers::class)->register($terms = Mockery::mock(Terms::class)->makePartial()); app(Providers::class)->register($assets = Mockery::mock(Assets::class)->makePartial()); - app(Providers::class)->register($users = Mockery::mock(Users::class)->makePartial()); $searchable = Mockery::mock(); $searchables = $this->makeSearchables(['searchables' => 'content']); // Check twice. // First time they'll all return false, so contains() will return false. - // Second time, assets will return true, so contains() will return true early, and users won't be checked. + // Second time, assets will return true, so contains() will return true early. $entries->shouldReceive('contains')->with($searchable)->twice()->andReturn(false, false); $terms->shouldReceive('contains')->with($searchable)->twice()->andReturn(false, false); $assets->shouldReceive('contains')->with($searchable)->twice()->andReturn(false, true); - $users->shouldReceive('contains')->with($searchable)->once()->andReturn(false); $this->assertFalse($searchables->contains($searchable)); $this->assertTrue($searchables->contains($searchable)); } #[Test] - public function content_searchables_include_entries_terms_assets_and_users() + public function content_searchables_include_entries_terms_and_assets() { app(Providers::class)->register($entries = Mockery::mock(Entries::class)->makePartial()); app(Providers::class)->register($terms = Mockery::mock(Terms::class)->makePartial()); app(Providers::class)->register($assets = Mockery::mock(Assets::class)->makePartial()); - app(Providers::class)->register($users = Mockery::mock(Users::class)->makePartial()); $entries->shouldReceive('provide')->andReturn(collect([$entryA = Entry::make(), $entryB = Entry::make()])); $terms->shouldReceive('provide')->andReturn(collect([$termA = Term::make(), $termB = Term::make()])); $assets->shouldReceive('provide')->andReturn(collect([$assetA = Asset::make(), $assetB = Asset::make()])); - $users->shouldReceive('provide')->andReturn(collect([$userA = User::make(), $userB = User::make()])); $searchables = $this->makeSearchables(['searchables' => 'content']); @@ -99,8 +95,6 @@ public function content_searchables_include_entries_terms_assets_and_users() $termB, $assetA, $assetB, - $userA, - $userB, ]; $items = $searchables->all(); @@ -108,6 +102,24 @@ public function content_searchables_include_entries_terms_assets_and_users() $this->assertEquals($everything, $items->all()); } + #[Test] + public function content_searchables_dont_include_users() + { + app(Providers::class)->register($entries = Mockery::mock(Entries::class)->makePartial()); + app(Providers::class)->register($terms = Mockery::mock(Terms::class)->makePartial()); + app(Providers::class)->register($assets = Mockery::mock(Assets::class)->makePartial()); + app(Providers::class)->register($users = Mockery::mock(Users::class)->makePartial()); + + $entries->shouldReceive('provide')->andReturn(collect([$entryA = Entry::make(), $entryB = Entry::make()])); + $terms->shouldReceive('provide')->andReturn(collect([$termA = Term::make(), $termB = Term::make()])); + $assets->shouldReceive('provide')->andReturn(collect([$assetA = Asset::make(), $assetB = Asset::make()])); + $users->shouldNotReceive('provide'); + + $searchables = $this->makeSearchables(['searchables' => 'content']); + + $searchables->all()->each(fn ($item) => $this->assertNotInstanceOf(User::class, $item)); + } + #[Test] public function it_gets_searchables_from_specific_providers() { From 01bca97d7f2959032f3198c6d20ff94f81d3a686 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 19 Nov 2025 15:26:50 +0000 Subject: [PATCH 04/13] Refactor `content` searchable... So it can combined with other searchables. --- src/Search/Searchables.php | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/Search/Searchables.php b/src/Search/Searchables.php index b72dbb03fb0..0a2109d82d4 100644 --- a/src/Search/Searchables.php +++ b/src/Search/Searchables.php @@ -34,13 +34,14 @@ private function makeProviders() throw new AllSearchablesNotSupported("'searchables' => 'all' is no longer supported. Please see the upgrade guide for more information: https://statamic.dev/getting-started/upgrade-guide/5-to-6"); } - if ($providers->contains('content')) { - return $manager->providers() - ->filter(fn ($_, $key) => in_array($key, ['collection', 'taxonomy', 'assets'])) - ->map(fn ($_, $key) => $manager->make($key, $this->index, ['*'])); - } - return $providers + ->flatMap(function ($key) { + if ($key === 'content') { + return ['collection:*', 'taxonomy:*', 'assets:*']; + } + + return [$key]; + }) ->map(fn ($key) => ['provider' => Str::before($key, ':'), 'key' => Str::after($key, ':')]) ->groupBy('provider') ->map(fn ($items, $provider) => $manager->make($provider, $this->index, $items->map->key->all())); From 8e4f10f5ba416f84531cdc56cff0802d0c6decbf Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 19 Nov 2025 16:21:35 +0000 Subject: [PATCH 05/13] Add runtime `cp` index To combine `content`, `users` and (soon) `addons`. --- .../CP/CommandPaletteController.php | 2 +- src/Search/Commands/Update.php | 1 + src/Search/IndexManager.php | 19 +++++++++++++++++-- 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/Http/Controllers/CP/CommandPaletteController.php b/src/Http/Controllers/CP/CommandPaletteController.php index e1e64987457..d1b56ea55ec 100644 --- a/src/Http/Controllers/CP/CommandPaletteController.php +++ b/src/Http/Controllers/CP/CommandPaletteController.php @@ -20,7 +20,7 @@ public function index() public function search(Request $request) { - return Search::index(locale: Site::selected()->handle()) + return Search::index(index: 'cp', locale: Site::selected()->handle()) ->ensureExists() ->search($request->query('q')) ->get() diff --git a/src/Search/Commands/Update.php b/src/Search/Commands/Update.php index dba5d49f5a3..fddcae55c68 100644 --- a/src/Search/Commands/Update.php +++ b/src/Search/Commands/Update.php @@ -73,6 +73,7 @@ private function getRequestedIndex() // They might have entered a name as it appears in the config, but if it // should be localized we'll get all of the localized versions. + // todo: do we need to do anything here to make our cp index work? if (collect(config('statamic.search.indexes'))->has($arg)) { return $this->indexes()->filter(fn ($index) => Str::startsWith($index->name(), $arg))->all(); } diff --git a/src/Search/IndexManager.php b/src/Search/IndexManager.php index 674fb270fea..f6702cf0e0a 100644 --- a/src/Search/IndexManager.php +++ b/src/Search/IndexManager.php @@ -20,7 +20,7 @@ protected function invalidImplementationMessage($name) public function all() { - return collect($this->app['config']['statamic.search.indexes'])->flatMap(function ($config, $name) { + return $this->indexesConfig()->flatMap(function ($config, $name) { $sites = $config['sites'] ?? null; if ($sites === 'all') { @@ -127,11 +127,26 @@ protected function callLocalizedCustomCreator(array $config, string $name, ?stri return $this->customCreators[$config['driver']]($this->app, $config, $name, $locale); } + private function indexesConfig() + { + $config = collect($this->app['config']['statamic.search.indexes']); + + if (! $config->has('cp')) { + $config->put('cp', [ + 'driver' => 'local', + 'searchables' => ['content', 'users'], + 'fields' => ['title'], + ]); + } + + return $config; + } + protected function getConfig($name) { $config = $this->app['config']; - if (! $index = $config["statamic.search.indexes.$name"]) { + if (! $index = $this->indexesConfig()->get($name)) { return null; } From 66e73cdfee4cae73628d3ce5bea493f1312aa8f8 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 19 Nov 2025 17:12:33 +0000 Subject: [PATCH 06/13] Allow addons to push searchables into `cp` index --- src/Search/IndexManager.php | 2 +- src/Search/Search.php | 6 ++++ src/Search/Searchables.php | 15 +++++++++ tests/Search/IndexManagerTest.php | 53 ++++++++++++++++++++++++++++++- tests/Search/SearchablesTest.php | 18 +++++++++++ 5 files changed, 92 insertions(+), 2 deletions(-) diff --git a/src/Search/IndexManager.php b/src/Search/IndexManager.php index f6702cf0e0a..9e7567bb8dd 100644 --- a/src/Search/IndexManager.php +++ b/src/Search/IndexManager.php @@ -134,7 +134,7 @@ private function indexesConfig() if (! $config->has('cp')) { $config->put('cp', [ 'driver' => 'local', - 'searchables' => ['content', 'users'], + 'searchables' => ['content', 'users', 'addons'], 'fields' => ['title'], ]); } diff --git a/src/Search/Search.php b/src/Search/Search.php index 890bc331728..a55d1c471f6 100644 --- a/src/Search/Search.php +++ b/src/Search/Search.php @@ -3,6 +3,7 @@ namespace Statamic\Search; use Statamic\Contracts\Search\Searchable; +use Statamic\Search\Searchables\Provider; use Statamic\Search\Searchables\Providers; class Search @@ -39,6 +40,11 @@ public function registerSearchableProvider($class) app(Providers::class)->register($class); } + public function addCpSearchable($searchable) + { + Searchables::addCpSearchable($searchable); + } + public function __call($method, $parameters) { return $this->index()->$method(...$parameters); diff --git a/src/Search/Searchables.php b/src/Search/Searchables.php index 0a2109d82d4..d111f9d5d92 100644 --- a/src/Search/Searchables.php +++ b/src/Search/Searchables.php @@ -8,6 +8,7 @@ use Illuminate\Support\LazyCollection; use Statamic\Contracts\Search\Searchable; use Statamic\Exceptions\AllSearchablesNotSupported; +use Statamic\Search\Searchables\Provider; use Statamic\Search\Searchables\Providers; use Statamic\Support\Arr; use Statamic\Support\Str; @@ -17,6 +18,7 @@ class Searchables protected $index; protected $providers; protected $manager; + protected static array $cpSearchables = []; public function __construct(Index $index) { @@ -24,6 +26,15 @@ public function __construct(Index $index) $this->providers = $this->makeProviders(); } + public static function addCpSearchable($searchable) + { + if (method_exists($searchable, 'handle')) { + $searchable = $searchable::handle(); + } + + static::$cpSearchables[] = $searchable; + } + private function makeProviders() { $manager = app(Providers::class); @@ -40,6 +51,10 @@ private function makeProviders() return ['collection:*', 'taxonomy:*', 'assets:*']; } + if ($key === 'addons') { + return static::$cpSearchables; + } + return [$key]; }) ->map(fn ($key) => ['provider' => Str::before($key, ':'), 'key' => Str::after($key, ':')]) diff --git a/tests/Search/IndexManagerTest.php b/tests/Search/IndexManagerTest.php index c00c86e16e2..0de50496a57 100644 --- a/tests/Search/IndexManagerTest.php +++ b/tests/Search/IndexManagerTest.php @@ -36,7 +36,7 @@ public function it_gets_indexes() $manager = new IndexManager($this->app); - $this->assertEquals(['foo', 'bar_en', 'bar_fr', 'baz_en', 'baz_fr', 'baz_de'], $manager->all()->map->name()->values()->all()); + $this->assertEquals(['foo', 'bar_en', 'bar_fr', 'baz_en', 'baz_fr', 'baz_de', 'cp'], $manager->all()->map->name()->values()->all()); $this->assertInstanceOf(NullIndex::class, $foo = $manager->index('foo')); $this->assertEquals('foo', $foo->name()); @@ -75,4 +75,55 @@ public function it_gets_indexes() $this->assertEquals('Search index [bar] has not been configured for the [de] site.', $e->getMessage()); } } + + #[Test] + public function it_builds_the_cp_index_if_it_doesnt_exist_in_the_config() + { + config(['statamic.search.indexes' => [ + 'default' => [ + 'driver' => 'local', + 'searchables' => 'content', + 'fields' => ['title'], + ], + ]]); + + $manager = new IndexManager($this->app); + + $this->assertEquals(['default', 'cp'], $manager->all()->map->name()->values()->all()); + + $this->assertEquals([ + 'fields' => ['title'], + 'path' => storage_path('statamic/search'), + 'driver' => 'local', + 'searchables' => ['content', 'users', 'addons'], + ], $manager->index('cp')->config()); + } + + #[Test] + public function it_uses_the_cp_index_from_the_config_if_it_exists() + { + config(['statamic.search.indexes' => [ + 'default' => [ + 'driver' => 'local', + 'searchables' => 'content', + 'fields' => ['title'], + ], + 'cp' => [ + 'driver' => 'local', + 'searchables' => ['collections:pages', 'collections:blog'], + 'fields' => ['title', 'excerpt'], + ], + ]]); + + $manager = new IndexManager($this->app); + + $this->assertEquals(['default', 'cp'], $manager->all()->map->name()->values()->all()); + + $this->assertEquals([ + 'fields' => ['title', 'excerpt'], + 'path' => storage_path('statamic/search'), + 'driver' => 'local', + 'searchables' => ['collections:pages', 'collections:blog'], + ], $manager->index('cp')->config()); + } } diff --git a/tests/Search/SearchablesTest.php b/tests/Search/SearchablesTest.php index b526034ae3b..b161020fb9d 100644 --- a/tests/Search/SearchablesTest.php +++ b/tests/Search/SearchablesTest.php @@ -120,6 +120,24 @@ public function content_searchables_dont_include_users() $searchables->all()->each(fn ($item) => $this->assertNotInstanceOf(User::class, $item)); } + #[Test] + public function can_add_cp_searchable() + { + $a = new TestCustomSearchable(['title' => 'Custom 1']); + $b = new TestCustomSearchable(['title' => 'Custom 2']); + app()->instance('all-custom-searchables', collect([$a, $b])); + + Search::registerSearchableProvider(TestCustomSearchables::class); + + $searchables = $this->makeSearchables(['searchables' => 'addons']); + $this->assertEquals([], $searchables->all()->all()); + + Search::addCpSearchable(TestCustomSearchables::class); + + $searchables = $this->makeSearchables(['searchables' => 'addons']); + $this->assertEquals([$a, $b], $searchables->all()->all()); + } + #[Test] public function it_gets_searchables_from_specific_providers() { From 530eadaa1385359d1acb1c427d416931677e3b56 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 19 Nov 2025 17:32:36 +0000 Subject: [PATCH 07/13] Wire up icons for CP search results --- src/Assets/Asset.php | 5 +++++ src/Auth/User.php | 5 +++++ src/Contracts/Search/Result.php | 2 ++ src/Http/Controllers/CP/CommandPaletteController.php | 2 +- src/Search/Result.php | 5 +++++ src/Search/Searchable.php | 5 +++++ src/Taxonomies/LocalizedTerm.php | 5 +++++ 7 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/Assets/Asset.php b/src/Assets/Asset.php index f681dd376f8..45b86201072 100644 --- a/src/Assets/Asset.php +++ b/src/Assets/Asset.php @@ -1131,6 +1131,11 @@ public function getCpSearchResultBadge(): string return $this->container()->title(); } + public function getCpSearchResultIcon() + { + return 'assets'; + } + public function warmPresets() { if (! $this->isImage()) { diff --git a/src/Auth/User.php b/src/Auth/User.php index 4228af32861..f9ff31376eb 100644 --- a/src/Auth/User.php +++ b/src/Auth/User.php @@ -447,6 +447,11 @@ public function getCpSearchResultBadge(): string return __('User'); } + public function getCpSearchResultIcon(): string + { + return 'users'; + } + public function getElevatedSessionMethod(): string { if (! config('statamic.webauthn.allow_password_login_with_passkey', true) && $this->passkeys()->isNotEmpty()) { diff --git a/src/Contracts/Search/Result.php b/src/Contracts/Search/Result.php index e63b226590f..e5b15cc05a4 100644 --- a/src/Contracts/Search/Result.php +++ b/src/Contracts/Search/Result.php @@ -32,4 +32,6 @@ public function getCpTitle(): string; public function getCpUrl(): string; public function getCpBadge(): string; + + public function getCpIcon(): string; } diff --git a/src/Http/Controllers/CP/CommandPaletteController.php b/src/Http/Controllers/CP/CommandPaletteController.php index d1b56ea55ec..6436f2f88e5 100644 --- a/src/Http/Controllers/CP/CommandPaletteController.php +++ b/src/Http/Controllers/CP/CommandPaletteController.php @@ -33,7 +33,7 @@ public function search(Request $request) ->url($result->getCpUrl()) ->badge($result->getCpBadge()) ->reference($result->getReference()) - // ->icon() // TODO: Make dynamic for entries/terms/users? + ->icon($result->getCpIcon()) ->toArray(); }) ->values(); diff --git a/src/Search/Result.php b/src/Search/Result.php index 281dc423bce..0a22bf393f0 100644 --- a/src/Search/Result.php +++ b/src/Search/Result.php @@ -130,6 +130,11 @@ public function getCpBadge(): string return $this->searchable->getCpSearchResultBadge(); } + public function getCpIcon(): string + { + return $this->searchable->getCpSearchResultIcon(); + } + public function get($key, $fallback = null) { if ($key === 'date' && method_exists($this->searchable, 'date')) { diff --git a/src/Search/Searchable.php b/src/Search/Searchable.php index 1b4b67cebae..38592947ac9 100644 --- a/src/Search/Searchable.php +++ b/src/Search/Searchable.php @@ -27,6 +27,11 @@ public function getCpSearchResultUrl() return $this->editUrl(); } + public function getCpSearchResultIcon() + { + return 'entry'; + } + public function toSearchResult(): Result { return new \Statamic\Search\Result($this, Str::before($this->reference(), '::')); diff --git a/src/Taxonomies/LocalizedTerm.php b/src/Taxonomies/LocalizedTerm.php index b6eb3137320..79c81dea9c1 100644 --- a/src/Taxonomies/LocalizedTerm.php +++ b/src/Taxonomies/LocalizedTerm.php @@ -557,6 +557,11 @@ public function getCpSearchResultBadge() return $this->taxonomy()->title(); } + public function getCpSearchResultIcon() + { + return 'taxonomies'; + } + public function getBulkAugmentationReferenceKey(): ?string { if ($this->augmentationReferenceKey) { From 095abc6b0f1965d5bcbeae296fd4544582d51b92 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Wed, 19 Nov 2025 17:41:00 +0000 Subject: [PATCH 08/13] Allow addons to push searchables into `content` index --- src/Search/Search.php | 5 +++++ src/Search/Searchables.php | 12 +++++++++++- tests/Search/SearchablesTest.php | 26 ++++++++++++++++++++++++++ 3 files changed, 42 insertions(+), 1 deletion(-) diff --git a/src/Search/Search.php b/src/Search/Search.php index a55d1c471f6..41ec2b3daa1 100644 --- a/src/Search/Search.php +++ b/src/Search/Search.php @@ -45,6 +45,11 @@ public function addCpSearchable($searchable) Searchables::addCpSearchable($searchable); } + public function addContentSearchable($searchable) + { + Searchables::addContentSearchable($searchable); + } + public function __call($method, $parameters) { return $this->index()->$method(...$parameters); diff --git a/src/Search/Searchables.php b/src/Search/Searchables.php index d111f9d5d92..3b7be14ad6c 100644 --- a/src/Search/Searchables.php +++ b/src/Search/Searchables.php @@ -19,6 +19,7 @@ class Searchables protected $providers; protected $manager; protected static array $cpSearchables = []; + protected static array $contentSearchables = []; public function __construct(Index $index) { @@ -35,6 +36,15 @@ public static function addCpSearchable($searchable) static::$cpSearchables[] = $searchable; } + public static function addContentSearchable($searchable) + { + if (method_exists($searchable, 'handle')) { + $searchable = $searchable::handle(); + } + + static::$contentSearchables[] = $searchable; + } + private function makeProviders() { $manager = app(Providers::class); @@ -48,7 +58,7 @@ private function makeProviders() return $providers ->flatMap(function ($key) { if ($key === 'content') { - return ['collection:*', 'taxonomy:*', 'assets:*']; + return ['collection:*', 'taxonomy:*', 'assets:*', ...static::$contentSearchables]; } if ($key === 'addons') { diff --git a/tests/Search/SearchablesTest.php b/tests/Search/SearchablesTest.php index b161020fb9d..172ce8f4966 100644 --- a/tests/Search/SearchablesTest.php +++ b/tests/Search/SearchablesTest.php @@ -120,6 +120,32 @@ public function content_searchables_dont_include_users() $searchables->all()->each(fn ($item) => $this->assertNotInstanceOf(User::class, $item)); } + #[Test] + public function can_add_content_searchable() + { + app(Providers::class)->register($entries = Mockery::mock(Entries::class)->makePartial()); + app(Providers::class)->register($terms = Mockery::mock(Terms::class)->makePartial()); + app(Providers::class)->register($assets = Mockery::mock(Assets::class)->makePartial()); + + $entries->shouldReceive('provide')->andReturn(collect([$entry = Entry::make()])); + $terms->shouldReceive('provide')->andReturn(collect([$term = Term::make()])); + $assets->shouldReceive('provide')->andReturn(collect([$asset = Asset::make()])); + + $a = new TestCustomSearchable(['title' => 'Custom 1']); + $b = new TestCustomSearchable(['title' => 'Custom 2']); + app()->instance('all-custom-searchables', collect([$a, $b])); + + Search::registerSearchableProvider(TestCustomSearchables::class); + + $searchables = $this->makeSearchables(['searchables' => 'content']); + $this->assertEquals([$entry, $term, $asset], $searchables->all()->all()); + + Search::addContentSearchable(TestCustomSearchables::class); + + $searchables = $this->makeSearchables(['searchables' => 'content']); + $this->assertEquals([$entry, $term, $asset, $a, $b], $searchables->all()->all()); + } + #[Test] public function can_add_cp_searchable() { From e61dea4853422ec7885fb30b43ea517f59d9adf5 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 20 Nov 2025 10:21:25 +0000 Subject: [PATCH 09/13] wip --- src/Search/Searchables.php | 10 ++++++++++ tests/Search/SearchablesTest.php | 8 ++++++++ 2 files changed, 18 insertions(+) diff --git a/src/Search/Searchables.php b/src/Search/Searchables.php index 3b7be14ad6c..ab891d704e5 100644 --- a/src/Search/Searchables.php +++ b/src/Search/Searchables.php @@ -36,6 +36,11 @@ public static function addCpSearchable($searchable) static::$cpSearchables[] = $searchable; } + public static function clearCpSearchables() + { + static::$cpSearchables = []; + } + public static function addContentSearchable($searchable) { if (method_exists($searchable, 'handle')) { @@ -45,6 +50,11 @@ public static function addContentSearchable($searchable) static::$contentSearchables[] = $searchable; } + public static function clearContentSearchables() + { + static::$contentSearchables = []; + } + private function makeProviders() { $manager = app(Providers::class); diff --git a/tests/Search/SearchablesTest.php b/tests/Search/SearchablesTest.php index 172ce8f4966..6fac9993a5e 100644 --- a/tests/Search/SearchablesTest.php +++ b/tests/Search/SearchablesTest.php @@ -45,6 +45,14 @@ public function setUp(): void AssetContainer::make('audio')->disk('audio')->save(); } + public function tearDown(): void + { + Searchables::clearCpSearchables(); + Searchables::clearContentSearchables(); + + parent::tearDown(); + } + #[Test] public function it_throws_an_exception_when_all_searchables_are_configured() { From 0dc0041f7ce9204c735550033bc77668a794e672 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 20 Nov 2025 10:23:45 +0000 Subject: [PATCH 10/13] formatting --- src/Search/Search.php | 1 - src/Search/Searchables.php | 1 - 2 files changed, 2 deletions(-) diff --git a/src/Search/Search.php b/src/Search/Search.php index 41ec2b3daa1..db24c8a9272 100644 --- a/src/Search/Search.php +++ b/src/Search/Search.php @@ -3,7 +3,6 @@ namespace Statamic\Search; use Statamic\Contracts\Search\Searchable; -use Statamic\Search\Searchables\Provider; use Statamic\Search\Searchables\Providers; class Search diff --git a/src/Search/Searchables.php b/src/Search/Searchables.php index ab891d704e5..f302ac4df47 100644 --- a/src/Search/Searchables.php +++ b/src/Search/Searchables.php @@ -8,7 +8,6 @@ use Illuminate\Support\LazyCollection; use Statamic\Contracts\Search\Searchable; use Statamic\Exceptions\AllSearchablesNotSupported; -use Statamic\Search\Searchables\Provider; use Statamic\Search\Searchables\Providers; use Statamic\Support\Arr; use Statamic\Support\Str; From 3a45009b3ca92c96947218d68bf54bf0bb2f4bc8 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 20 Nov 2025 10:28:46 +0000 Subject: [PATCH 11/13] Missed a spot --- tests/Search/Searchables/TermsTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Search/Searchables/TermsTest.php b/tests/Search/Searchables/TermsTest.php index fb73fd332f1..6706bf508ce 100644 --- a/tests/Search/Searchables/TermsTest.php +++ b/tests/Search/Searchables/TermsTest.php @@ -157,9 +157,9 @@ public static function termsProvider() ], ], - 'all, french' => [ + 'content, french' => [ 'fr', - ['searchables' => 'all'], + ['searchables' => 'content'], [ 'term::tags::alfa::fr', 'term::tags::bravo::fr', From 28fe003b79957b429cb4130ae1b2ace58b9a4ceb Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 20 Nov 2025 11:22:30 +0000 Subject: [PATCH 12/13] Ensure `cp` index is included in check --- src/Search/Commands/Update.php | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Search/Commands/Update.php b/src/Search/Commands/Update.php index fddcae55c68..3e6ba18611a 100644 --- a/src/Search/Commands/Update.php +++ b/src/Search/Commands/Update.php @@ -73,8 +73,7 @@ private function getRequestedIndex() // They might have entered a name as it appears in the config, but if it // should be localized we'll get all of the localized versions. - // todo: do we need to do anything here to make our cp index work? - if (collect(config('statamic.search.indexes'))->has($arg)) { + if (collect(config('statamic.search.indexes'))->put('cp', [])->has($arg)) { return $this->indexes()->filter(fn ($index) => Str::startsWith($index->name(), $arg))->all(); } From 10ae81f80e2548c57bf8d5438a8eaccac9ed7d67 Mon Sep 17 00:00:00 2001 From: Duncan McClean Date: Thu, 20 Nov 2025 11:25:24 +0000 Subject: [PATCH 13/13] Move message into exception class --- src/Exceptions/AllSearchablesNotSupported.php | 5 ++++- src/Search/Searchables.php | 2 +- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Exceptions/AllSearchablesNotSupported.php b/src/Exceptions/AllSearchablesNotSupported.php index cec9c94368a..4b2b1091512 100644 --- a/src/Exceptions/AllSearchablesNotSupported.php +++ b/src/Exceptions/AllSearchablesNotSupported.php @@ -4,5 +4,8 @@ class AllSearchablesNotSupported extends \Exception { - // + public function __construct() + { + parent::__construct("'searchables' => 'all' is no longer supported. Please see the upgrade guide for more information: https://statamic.dev/getting-started/upgrade-guide/5-to-6"); + } } diff --git a/src/Search/Searchables.php b/src/Search/Searchables.php index f302ac4df47..4df925d39dd 100644 --- a/src/Search/Searchables.php +++ b/src/Search/Searchables.php @@ -61,7 +61,7 @@ private function makeProviders() $providers = collect(Arr::wrap($this->index->config()['searchables'] ?? [])); if ($providers->contains('all')) { - throw new AllSearchablesNotSupported("'searchables' => 'all' is no longer supported. Please see the upgrade guide for more information: https://statamic.dev/getting-started/upgrade-guide/5-to-6"); + throw new AllSearchablesNotSupported(); } return $providers