From b5f46a1d7826dfe824338c962ef294643b73c6ed Mon Sep 17 00:00:00 2001 From: Yurij Finiv Date: Fri, 25 Sep 2026 23:13:43 +0300 Subject: [PATCH 01/10] Normalize search result values safely --- src/Search/SearchRegistry.php | 63 ++++++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/src/Search/SearchRegistry.php b/src/Search/SearchRegistry.php index 86d78da..b848a4b 100644 --- a/src/Search/SearchRegistry.php +++ b/src/Search/SearchRegistry.php @@ -3,6 +3,7 @@ namespace Step2dev\LazyAdmin\Search; use Closure; +use Stringable; class SearchRegistry { @@ -36,16 +37,23 @@ public function search(string $query, ?object $user, int $limitPerProvider = 8): $items = ($definition['provider'])($query, $limitPerProvider); foreach ($items as $item) { - if (! is_array($item) || empty($item['title']) || empty($item['url'])) { + if (! is_array($item)) { + continue; + } + + $title = $this->stringValue($item['title'] ?? null); + $url = $this->stringValue($item['url'] ?? null); + + if ($title === null || $url === null) { continue; } $results[] = [ 'provider' => $definition['id'], - 'title' => (string) $item['title'], - 'url' => (string) $item['url'], - 'description' => isset($item['description']) ? (string) $item['description'] : null, - 'type' => isset($item['type']) ? (string) $item['type'] : null, + 'title' => $title, + 'url' => $url, + 'description' => $this->stringValue($item['description'] ?? null), + 'type' => $this->stringValue($item['type'] ?? null), ]; } } @@ -53,6 +61,51 @@ public function search(string $query, ?object $user, int $limitPerProvider = 8): return $results; } + private function stringValue(mixed $value): ?string + { + if (is_string($value)) { + $value = trim($value); + + return $value !== '' ? $value : null; + } + + if (is_int($value) || is_float($value)) { + return (string) $value; + } + + if ($value instanceof Stringable) { + $value = trim((string) $value); + + return $value !== '' ? $value : null; + } + + if (! is_array($value)) { + return null; + } + + $locales = array_values(array_unique(array_filter([ + app()->getLocale(), + config('app.fallback_locale'), + 'en', + ], 'is_string'))); + + foreach ($locales as $locale) { + $candidate = $value[$locale] ?? null; + + if (is_string($candidate) && trim($candidate) !== '') { + return trim($candidate); + } + } + + foreach ($value as $candidate) { + if (is_string($candidate) && trim($candidate) !== '') { + return trim($candidate); + } + } + + return null; + } + private function allowed(?object $user, ?string $permission): bool { if ($permission === null || ! config('lazy.admin.permissions.enforce', true)) { From 8b4384f04e5b5581ad0c7f720ac690380b439c45 Mon Sep 17 00:00:00 2001 From: Yurij Finiv Date: Fri, 25 Sep 2026 23:13:46 +0300 Subject: [PATCH 02/10] Use namespaced search result labels --- src/LazyAdminServiceProvider.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/LazyAdminServiceProvider.php b/src/LazyAdminServiceProvider.php index 3335ba4..4ce8af6 100644 --- a/src/LazyAdminServiceProvider.php +++ b/src/LazyAdminServiceProvider.php @@ -321,7 +321,7 @@ public function packageBooted(): void : $page->slug, 'description' => $page->path(), 'url' => route($prefix.'.page.index', ['search' => $page->slug]), - 'type' => __('Page'), + 'type' => __('lazy-admin::search.types.page'), ]; } @@ -351,8 +351,8 @@ public function packageBooted(): void ->map(fn ($user): array => [ 'title' => (string) $user->getAttribute('name'), 'description' => (string) $user->getAttribute('email'), - 'url' => route($prefix.'.user.show', $user), - 'type' => __('User'), + 'url' => route($prefix.'.user.show', $user->getKey()), + 'type' => __('lazy-admin::search.types.user'), ]) ->all(); }, @@ -372,7 +372,7 @@ public function packageBooted(): void 'title' => $section['label'], 'description' => $section['description'], 'url' => route($prefix.'.setting.index', ['section' => $section['id']]), - 'type' => __('Setting'), + 'type' => __('lazy-admin::search.types.setting'), ]) ->values() ->all(); From 2e0dd009db98f7bfd75613d0d13a4a913ec8175f Mon Sep 17 00:00:00 2001 From: Yurij Finiv Date: Fri, 25 Sep 2026 23:13:48 +0300 Subject: [PATCH 03/10] Namespace search page title --- src/Http/Livewire/Search/Page.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Http/Livewire/Search/Page.php b/src/Http/Livewire/Search/Page.php index de2b42c..c6a6ac8 100644 --- a/src/Http/Livewire/Search/Page.php +++ b/src/Http/Livewire/Search/Page.php @@ -9,7 +9,7 @@ use Livewire\Component; use Step2dev\LazyAdmin\Search\SearchRegistry; -#[Layout('lazy::livewire-layout', ['title' => 'Search'])] +#[Layout('lazy::livewire-layout', ['title' => 'lazy-admin::search.title'])] class Page extends Component { #[Url(as: 'q', history: true)] From 5dbaa369341f519b9571bbff369770cde6740033 Mon Sep 17 00:00:00 2001 From: Yurij Finiv Date: Fri, 25 Sep 2026 23:13:50 +0300 Subject: [PATCH 04/10] Namespace search page copy --- resources/views/search/index.blade.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/views/search/index.blade.php b/resources/views/search/index.blade.php index 99750ff..d7c0fc7 100644 --- a/resources/views/search/index.blade.php +++ b/resources/views/search/index.blade.php @@ -5,8 +5,8 @@ wire:model.live.debounce.250ms="query" minlength="2" maxlength="100" - :placeholder="__('Search admin content')" - aria-label="{{ __('Search admin content') }}" + :placeholder="__('lazy-admin::search.placeholder')" + aria-label="{{ __('lazy-admin::search.placeholder') }}" autofocus />
@@ -15,9 +15,9 @@
@if(mb_strlen($query) < 2) - + @elseif($results === []) - + @else
@foreach($results as $result) From d51ed631d88a7637de41c7fe50911b50c56b5911 Mon Sep 17 00:00:00 2001 From: Yurij Finiv Date: Fri, 25 Sep 2026 23:13:52 +0300 Subject: [PATCH 05/10] Namespace header search copy --- resources/views/search/header.blade.php | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/resources/views/search/header.blade.php b/resources/views/search/header.blade.php index 7669293..a0c6167 100644 --- a/resources/views/search/header.blade.php +++ b/resources/views/search/header.blade.php @@ -3,8 +3,8 @@ type="search" wire:model.live.debounce.250ms="query" @focus="open = true" - :placeholder="__('Search')" - aria-label="{{ __('Search') }}" + :placeholder="__('lazy-admin::search.short_placeholder')" + aria-label="{{ __('lazy-admin::search.short_placeholder') }}" class="w-72" minlength="2" maxlength="100" @@ -32,7 +32,7 @@ class="h-auto w-full justify-start px-3 py-2 text-left" @empty - + @endforelse From 6d4c880acf16a77f642b4e1d9582e8b76a79100c Mon Sep 17 00:00:00 2001 From: Yurij Finiv Date: Fri, 25 Sep 2026 23:13:54 +0300 Subject: [PATCH 06/10] Cover array search provider values --- tests/Feature/AdminPlatformTest.php | 39 +++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/tests/Feature/AdminPlatformTest.php b/tests/Feature/AdminPlatformTest.php index 514d30d..dc5828b 100644 --- a/tests/Feature/AdminPlatformTest.php +++ b/tests/Feature/AdminPlatformTest.php @@ -62,6 +62,45 @@ ->and($results[0]['url'])->toBe('/orders/1'); }); +it('normalizes localized array values returned by search providers', function (): void { + app()->setLocale('uk'); + config()->set('app.fallback_locale', 'en'); + + $registry = new SearchRegistry; + + $registry->register( + id: 'localized', + provider: fn (): array => [[ + 'title' => ['uk' => 'Сторінка', 'en' => 'Page'], + 'url' => '/pages/1', + 'description' => ['uk' => 'Опис', 'en' => 'Description'], + 'type' => ['uk' => 'Сторінка', 'en' => 'Page'], + ]], + ); + + $results = $registry->search('page', null); + + expect($results) + ->toHaveCount(1) + ->and($results[0]['title'])->toBe('Сторінка') + ->and($results[0]['description'])->toBe('Опис') + ->and($results[0]['type'])->toBe('Сторінка'); +}); + +it('skips malformed search results instead of casting arrays blindly', function (): void { + $registry = new SearchRegistry; + + $registry->register( + id: 'malformed', + provider: fn (): array => [[ + 'title' => ['nested' => ['value']], + 'url' => '/pages/1', + ]], + ); + + expect($registry->search('page', null))->toBe([]); +}); + it('allows modules to register settings sections', function (): void { $registry = new SettingsRegistry; From 8d71a1d429e2577844a415d84914604645bb8b3f Mon Sep 17 00:00:00 2001 From: Yurij Finiv Date: Fri, 25 Sep 2026 23:13:56 +0300 Subject: [PATCH 07/10] Use namespaced header search translation --- tests/Feature/HeaderTest.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Feature/HeaderTest.php b/tests/Feature/HeaderTest.php index f5c112f..0e18390 100644 --- a/tests/Feature/HeaderTest.php +++ b/tests/Feature/HeaderTest.php @@ -5,6 +5,6 @@ expect($html) ->toContain('menu-toggle') - ->toContain('placeholder="'.__('Search').'"') + ->toContain('placeholder="'.__('lazy-admin::search.short_placeholder').'"') ->toContain(config('lazy.admin.logo', '/main.svg')); }); From 6f7654f62a33cf611f58e4d6f06e669ac75bde4d Mon Sep 17 00:00:00 2001 From: Yurij Finiv Date: Fri, 25 Sep 2026 23:13:58 +0300 Subject: [PATCH 08/10] Add namespaced search translations --- resources/lang/en/search.php | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) create mode 100644 resources/lang/en/search.php diff --git a/resources/lang/en/search.php b/resources/lang/en/search.php new file mode 100644 index 0000000..77eec71 --- /dev/null +++ b/resources/lang/en/search.php @@ -0,0 +1,16 @@ + 'Search', + 'short_placeholder' => 'Search', + 'placeholder' => 'Search admin content', + 'min_length' => 'Enter at least two characters.', + 'empty' => 'No results found.', + 'no_results' => 'No results', + 'view_all' => 'View all results', + 'types' => [ + 'page' => 'Page', + 'user' => 'User', + 'setting' => 'Setting', + ], +]; From f822423bdf1341013ab92b57743053497251f97b Mon Sep 17 00:00:00 2001 From: Yurij Finiv Date: Fri, 25 Sep 2026 23:14:45 +0300 Subject: [PATCH 09/10] Fix search registry test provider signatures --- tests/Feature/AdminPlatformTest.php | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/Feature/AdminPlatformTest.php b/tests/Feature/AdminPlatformTest.php index dc5828b..efa8079 100644 --- a/tests/Feature/AdminPlatformTest.php +++ b/tests/Feature/AdminPlatformTest.php @@ -70,7 +70,7 @@ $registry->register( id: 'localized', - provider: fn (): array => [[ + provider: fn (string $query, int $limit): array => [[ 'title' => ['uk' => 'Сторінка', 'en' => 'Page'], 'url' => '/pages/1', 'description' => ['uk' => 'Опис', 'en' => 'Description'], @@ -92,7 +92,7 @@ $registry->register( id: 'malformed', - provider: fn (): array => [[ + provider: fn (string $query, int $limit): array => [[ 'title' => ['nested' => ['value']], 'url' => '/pages/1', ]], From 12a97379b2de3db7313a6944a3d7c7e7e645ec99 Mon Sep 17 00:00:00 2001 From: Yurij Finiv Date: Fri, 25 Sep 2026 23:14:48 +0300 Subject: [PATCH 10/10] Handle missing user show route in global search --- src/LazyAdminServiceProvider.php | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/LazyAdminServiceProvider.php b/src/LazyAdminServiceProvider.php index 4ce8af6..f93cc97 100644 --- a/src/LazyAdminServiceProvider.php +++ b/src/LazyAdminServiceProvider.php @@ -342,6 +342,10 @@ public function packageBooted(): void return []; } + $showRoute = $prefix.'.user.show'; + $indexRoute = $prefix.'.user.index'; + $hasShowRoute = RouteFacade::has($showRoute); + return $model::query() ->where(fn ($builder) => $builder ->where('name', 'like', '%'.$query.'%') @@ -351,7 +355,9 @@ public function packageBooted(): void ->map(fn ($user): array => [ 'title' => (string) $user->getAttribute('name'), 'description' => (string) $user->getAttribute('email'), - 'url' => route($prefix.'.user.show', $user->getKey()), + 'url' => $hasShowRoute + ? route($showRoute, $user->getKey()) + : route($indexRoute), 'type' => __('lazy-admin::search.types.user'), ]) ->all();