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'));
});