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', + ], +]; 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 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) 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)] diff --git a/src/LazyAdminServiceProvider.php b/src/LazyAdminServiceProvider.php index 3335ba4..f93cc97 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'), ]; } @@ -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,8 +355,10 @@ 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' => $hasShowRoute + ? route($showRoute, $user->getKey()) + : route($indexRoute), + 'type' => __('lazy-admin::search.types.user'), ]) ->all(); }, @@ -372,7 +378,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(); 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)) { diff --git a/tests/Feature/AdminPlatformTest.php b/tests/Feature/AdminPlatformTest.php index 514d30d..efa8079 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 (string $query, int $limit): 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 (string $query, int $limit): 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; 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')); });