diff --git a/app/Models/Asset.php b/app/Models/Asset.php index ac431253bc..42c97a5cd7 100644 --- a/app/Models/Asset.php +++ b/app/Models/Asset.php @@ -947,8 +947,10 @@ class Asset extends Depreciable ->orWhere('assets_users.first_name', 'LIKE', '%'.$term.'%') ->orWhere('assets_users.last_name', 'LIKE', '%'.$term.'%') ->orWhere('assets_users.username', 'LIKE', '%'.$term.'%') - ->orWhereRaw('CONCAT('.DB::getTablePrefix().'assets_users.first_name," ",'.DB::getTablePrefix().'assets_users.last_name) LIKE ?', ["%$term%"]); - + ->orWhereMultipleColumns([ + 'assets_users.first_name', + 'assets_users.last_name', + ], $term); } /** @@ -1343,7 +1345,10 @@ class Asset extends Depreciable })->orWhere(function ($query) use ($search) { $query->where('assets_users.first_name', 'LIKE', '%'.$search.'%') ->orWhere('assets_users.last_name', 'LIKE', '%'.$search.'%') - ->orWhereRaw('CONCAT('.DB::getTablePrefix().'assets_users.first_name," ",'.DB::getTablePrefix().'assets_users.last_name) LIKE ?', ["%$search%"]) + ->orWhereMultipleColumns([ + 'assets_users.first_name', + 'assets_users.last_name', + ], $search) ->orWhere('assets_users.username', 'LIKE', '%'.$search.'%') ->orWhere('assets_locations.name', 'LIKE', '%'.$search.'%') ->orWhere('assigned_assets.name', 'LIKE', '%'.$search.'%'); diff --git a/app/Models/Traits/Searchable.php b/app/Models/Traits/Searchable.php index a7feb62957..06e05348fb 100644 --- a/app/Models/Traits/Searchable.php +++ b/app/Models/Traits/Searchable.php @@ -5,6 +5,7 @@ namespace App\Models\Traits; use App\Models\Asset; use App\Models\CustomField; use Illuminate\Database\Eloquent\Builder; +use Illuminate\Support\Facades\DB; /** * This trait allows for cleaner searching of models, @@ -164,7 +165,13 @@ trait Searchable } // I put this here because I only want to add the concat one time in the end of the user relation search if($relation == 'user') { - $query->orWhereRaw('CONCAT (users.first_name, " ", users.last_name) LIKE ?', ["%{$term}%"]); + $query->orWhereRaw( + $this->buildMultipleColumnSearch([ + 'users.first_name', + 'users.last_name', + ]), + ["%{$term}%"] + ); } }); } @@ -257,4 +264,37 @@ trait Searchable return $related->getTable(); } + + /** + * Builds a search string for either MySQL or sqlite by separating the provided columns with a space. + * + * @param array $columns Columns to include in search string. + * @return string + */ + private function buildMultipleColumnSearch(array $columns): string + { + $mappedColumns = collect($columns)->map(fn($column) => DB::getTablePrefix() . $column)->toArray(); + + $driver = config('database.connections.' . config('database.default') . '.driver'); + + if ($driver === 'sqlite') { + return implode("||' '||", $mappedColumns) . ' LIKE ?'; + } + + // Default to MySQL's concatenation method + return 'CONCAT(' . implode('," ",', $mappedColumns) . ') LIKE ?'; + } + + /** + * Search a string across multiple columns separated with a space. + * + * @param Builder $query + * @param array $columns - Columns to include in search string. + * @param $term + * @return Builder + */ + public function scopeOrWhereMultipleColumns($query, array $columns, $term) + { + return $query->orWhereRaw($this->buildMultipleColumnSearch($columns), ["%{$term}%"]); + } } diff --git a/app/Models/User.php b/app/Models/User.php index 1e63ebad71..98a3ec346b 100644 --- a/app/Models/User.php +++ b/app/Models/User.php @@ -644,14 +644,14 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo */ public function scopeSimpleNameSearch($query, $search) { - $query = $query->where('first_name', 'LIKE', '%'.$search.'%') - ->orWhere('last_name', 'LIKE', '%'.$search.'%') - ->orWhereRaw('CONCAT('.DB::getTablePrefix().'users.first_name," ",'.DB::getTablePrefix().'users.last_name) LIKE ?', ["%{$search}%"]); - - return $query; + return $query->where('first_name', 'LIKE', '%' . $search . '%') + ->orWhere('last_name', 'LIKE', '%' . $search . '%') + ->orWhereMultipleColumns([ + 'users.first_name', + 'users.last_name', + ], $search); } - /** * Run additional, advanced searches. * @@ -660,9 +660,11 @@ class User extends SnipeModel implements AuthenticatableContract, AuthorizableCo * @return \Illuminate\Database\Eloquent\Builder */ public function advancedTextSearch(Builder $query, array $terms) { - foreach($terms as $term) { - $query = $query->orWhereRaw('CONCAT('.DB::getTablePrefix().'users.first_name," ",'.DB::getTablePrefix().'users.last_name) LIKE ?', ["%{$term}%"]); + $query->orWhereMultipleColumns([ + 'users.first_name', + 'users.last_name', + ], $term); } return $query; diff --git a/database/seeders/CustomFieldSeeder.php b/database/seeders/CustomFieldSeeder.php index 8776872644..551e05f40f 100644 --- a/database/seeders/CustomFieldSeeder.php +++ b/database/seeders/CustomFieldSeeder.php @@ -38,24 +38,34 @@ class CustomFieldSeeder extends Seeder [ 'custom_field_id' => '1', 'custom_fieldset_id' => '1', + 'order' => 0, + 'required' => 0, ], [ 'custom_field_id' => '2', 'custom_fieldset_id' => '1', + 'order' => 0, + 'required' => 0, ], [ - 'custom_field_id' => '3', - 'custom_fieldset_id' => '2', + 'custom_field_id' => '3', + 'custom_fieldset_id' => '2', + 'order' => 0, + 'required' => 0, ], [ - 'custom_field_id' => '4', - 'custom_fieldset_id' => '2', + 'custom_field_id' => '4', + 'custom_fieldset_id' => '2', + 'order' => 0, + 'required' => 0, ], [ - 'custom_field_id' => '5', - 'custom_fieldset_id' => '2', + 'custom_field_id' => '5', + 'custom_fieldset_id' => '2', + 'order' => 0, + 'required' => 0, ], - ]); + ]); } } diff --git a/public/css/build/app.css b/public/css/build/app.css index 623ae2df1d..b07a73e0ae 100644 Binary files a/public/css/build/app.css and b/public/css/build/app.css differ diff --git a/public/css/build/overrides.css b/public/css/build/overrides.css index 2c4f57f858..b6864a763b 100644 Binary files a/public/css/build/overrides.css and b/public/css/build/overrides.css differ diff --git a/public/css/dist/all.css b/public/css/dist/all.css index 6db24038e1..1cdb2ea992 100644 Binary files a/public/css/dist/all.css and b/public/css/dist/all.css differ diff --git a/public/mix-manifest.json b/public/mix-manifest.json index 61e404923a..1ec21a1336 100644 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -1,8 +1,8 @@ { "/js/build/app.js": "/js/build/app.js?id=7caeae38608edd96421f8ef59d33f5f6", "/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=f677207c6cf9678eb539abecb408c374", - "/css/build/overrides.css": "/css/build/overrides.css?id=97a58ce3a89cd0043a1c54ecf63d4686", - "/css/build/app.css": "/css/build/app.css?id=02dbcc25fce08e9b9a9b285883821805", + "/css/build/overrides.css": "/css/build/overrides.css?id=eb44bf9eb12277e0dc680c6d498ee6af", + "/css/build/app.css": "/css/build/app.css?id=62fd4966017ec9d8740b21869eca4302", "/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=dc383f8560a8d4adb51d44fb4043e03b", "/css/dist/skins/skin-orange.css": "/css/dist/skins/skin-orange.css?id=6f0563e726c2fe4fab4026daaa5bfdf2", "/css/dist/skins/skin-orange-dark.css": "/css/dist/skins/skin-orange-dark.css?id=e6e53eef152bba01a4c666a4d8b01117", @@ -18,7 +18,7 @@ "/css/dist/skins/skin-green.css": "/css/dist/skins/skin-green.css?id=0a82a6ae6bb4e58fe62d162c4fb50397", "/css/dist/skins/skin-contrast.css": "/css/dist/skins/skin-contrast.css?id=da6c7997d9de2f8329142399f0ce50da", "/css/dist/skins/skin-red.css": "/css/dist/skins/skin-red.css?id=44bf834f2110504a793dadec132a5898", - "/css/dist/all.css": "/css/dist/all.css?id=999cf8c1432f7baff31b61a04a9a8485", + "/css/dist/all.css": "/css/dist/all.css?id=654fb4ff3970fdd2f3094090ab18d098", "/css/dist/signature-pad.css": "/css/dist/signature-pad.css?id=6a89d3cd901305e66ced1cf5f13147f7", "/css/dist/signature-pad.min.css": "/css/dist/signature-pad.min.css?id=6a89d3cd901305e66ced1cf5f13147f7", "/css/webfonts/fa-brands-400.ttf": "/css/webfonts/fa-brands-400.ttf?id=e2e2b1797606a266ed55549f5bb5a179", diff --git a/resources/assets/less/overrides.less b/resources/assets/less/overrides.less index d4bf509e89..ba85674e7b 100644 --- a/resources/assets/less/overrides.less +++ b/resources/assets/less/overrides.less @@ -844,8 +844,12 @@ input[type="radio"]:checked::before { grid-template-columns: .1em auto; gap: 1.5em; } + .nav-tabs-custom > .nav-tabs > li { z-index:1; + +.select2-container .select2-search--inline .select2-search__field{ + padding-left:15px; } /** --------------------------------------- **/ diff --git a/tests/Feature/Api/Users/UsersForSelectListTest.php b/tests/Feature/Api/Users/UsersForSelectListTest.php index 6ab5bf9a85..8cdf700f04 100644 --- a/tests/Feature/Api/Users/UsersForSelectListTest.php +++ b/tests/Feature/Api/Users/UsersForSelectListTest.php @@ -30,6 +30,19 @@ class UsersForSelectListTest extends TestCase ->assertJson(fn(AssertableJson $json) => $json->has('results', 3)->etc()); } + public function testUsersCanBeSearchedByFirstAndLastName() + { + User::factory()->create(['first_name' => 'Luke', 'last_name' => 'Skywalker']); + + Passport::actingAs(User::factory()->create()); + $response = $this->getJson(route('api.users.selectlist', ['search' => 'luke sky']))->assertOk(); + + $results = collect($response->json('results')); + + $this->assertEquals(1, $results->count()); + $this->assertTrue($results->pluck('text')->contains(fn($text) => str_contains($text, 'Luke'))); + } + public function testUsersScopedToCompanyWhenMultipleFullCompanySupportEnabled() { $this->settings->enableMultipleFullCompanySupport(); diff --git a/tests/Feature/Api/Users/UsersSearchTest.php b/tests/Feature/Api/Users/UsersSearchTest.php new file mode 100644 index 0000000000..33f77196f3 --- /dev/null +++ b/tests/Feature/Api/Users/UsersSearchTest.php @@ -0,0 +1,28 @@ +create(['first_name' => 'Luke', 'last_name' => 'Skywalker']); + User::factory()->create(['first_name' => 'Darth', 'last_name' => 'Vader']); + + Passport::actingAs(User::factory()->viewUsers()->create()); + $response = $this->getJson(route('api.users.index', ['search' => 'luke sky']))->assertOk(); + + $results = collect($response->json('rows')); + + $this->assertEquals(1, $results->count()); + $this->assertTrue($results->pluck('name')->contains(fn($text) => str_contains($text, 'Luke'))); + $this->assertFalse($results->pluck('name')->contains(fn($text) => str_contains($text, 'Darth'))); + } +}