diff --git a/app/Http/Controllers/Api/AccessoriesController.php b/app/Http/Controllers/Api/AccessoriesController.php index 91a6c5596b..263bd2086f 100644 --- a/app/Http/Controllers/Api/AccessoriesController.php +++ b/app/Http/Controllers/Api/AccessoriesController.php @@ -150,7 +150,7 @@ class AccessoriesController extends Controller public function show($id) { $this->authorize('view', Accessory::class); - $accessory = Accessory::findOrFail($id); + $accessory = Accessory::withCount('users as users_count')->findOrFail($id); return (new AccessoriesTransformer)->transformAccessory($accessory); } diff --git a/app/Http/Controllers/Api/AssetModelsController.php b/app/Http/Controllers/Api/AssetModelsController.php index a9ef48adc0..cf13d24fc4 100644 --- a/app/Http/Controllers/Api/AssetModelsController.php +++ b/app/Http/Controllers/Api/AssetModelsController.php @@ -63,7 +63,7 @@ class AssetModelsController extends Controller 'models.deleted_at', 'models.updated_at', ]) - ->with('category', 'depreciation', 'manufacturer', 'fieldset') + ->with('category', 'depreciation', 'manufacturer', 'fieldset.fields.defaultValues') ->withCount('assets as assets_count'); if ($request->input('status')=='deleted') { 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/config/version.php b/config/version.php index 9c5411191a..6c0b1e6b0c 100644 --- a/config/version.php +++ b/config/version.php @@ -1,11 +1,10 @@ 'v6.1.1-pre', - 'full_app_version' => 'v6.1.1-pre - build 10653-g11cd875c6', - 'build_version' => '10653', + 'full_app_version' => 'v6.1.1-pre - build 10727-gf1b4bba3a', + 'build_version' => '10727', 'prerelease_version' => '', - 'hash_version' => 'g11cd875c6', - 'full_hash' => 'v6.1.1-pre-411-g11cd875c6', + 'hash_version' => 'gf1b4bba3a', + 'full_hash' => 'v6.1.1-pre-485-gf1b4bba3a', 'branch' => 'master', -); - +); \ No newline at end of file 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 2be4064bb4..8ada1e1aff 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 54f0df97a6..d7f94759dc 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 90bb4b77b3..abc977dde5 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 3d6c3a781c..9901e55f34 100644 --- a/public/mix-manifest.json +++ b/public/mix-manifest.json @@ -1,8 +1,8 @@ { "/js/build/app.js": "/js/build/app.js?id=59ddb05ca277a4e3a8b8cf3c2f5c01b8", "/css/dist/skins/skin-blue.css": "/css/dist/skins/skin-blue.css?id=392cc93cfc0be0349bab9697669dd091", - "/css/build/overrides.css": "/css/build/overrides.css?id=41ee7867cae2cd3e6b2f56afdd46a34b", - "/css/build/app.css": "/css/build/app.css?id=ca9416d887fb59cc204dfaf440b47715", + "/css/build/overrides.css": "/css/build/overrides.css?id=82747ab3fdd0858511791ced2f494fc1", + "/css/build/app.css": "/css/build/app.css?id=363957ca759db3c5ba2dc23afc90a4bc", "/css/build/AdminLTE.css": "/css/build/AdminLTE.css?id=f25c77ed07053646a42e9c19923d24fa", "/css/dist/skins/skin-orange.css": "/css/dist/skins/skin-orange.css?id=268041e902b019730c23ee3875838005", "/css/dist/skins/skin-orange-dark.css": "/css/dist/skins/skin-orange-dark.css?id=d409d9b1a3b69247df8b98941ba06e33", @@ -18,7 +18,7 @@ "/css/dist/skins/skin-green.css": "/css/dist/skins/skin-green.css?id=b48f4d8af0e1ca5621c161e93951109f", "/css/dist/skins/skin-contrast.css": "/css/dist/skins/skin-contrast.css?id=f0fbbb0ac729ea092578fb05ca615460", "/css/dist/skins/skin-red.css": "/css/dist/skins/skin-red.css?id=b9a74ec0cd68f83e7480d5ae39919beb", - "/css/dist/all.css": "/css/dist/all.css?id=cfa427de31c9b05b0626527fdfa20fa0", + "/css/dist/all.css": "/css/dist/all.css?id=b1fab88c57be0a6a2590c0a47ef4e835", "/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 e8dc67ce3d..fff4d01dee 100644 --- a/resources/assets/less/overrides.less +++ b/resources/assets/less/overrides.less @@ -673,6 +673,9 @@ th.css-accessory > .th-inner::before } @media screen and (max-width: 511px){ + .tab-content .tab-pane .alert-block { + margin-top: 120px + } .sidebar-menu{ margin-top:160px; } @@ -842,6 +845,14 @@ input[type="radio"]:checked::before { gap: 1.5em; } +.nav-tabs-custom > .nav-tabs > li { + z-index: 1; +} + +.select2-container .select2-search--inline .select2-search__field{ + padding-left:15px; +} + /** --------------------------------------- **/ /** End checkbox styles to replace iCheck **/ /** --------------------------------------- **/ \ No newline at end of file diff --git a/resources/views/notifications/markdown/checkin-accessory.blade.php b/resources/views/notifications/markdown/checkin-accessory.blade.php index cf1536518b..98a49bf7e2 100644 --- a/resources/views/notifications/markdown/checkin-accessory.blade.php +++ b/resources/views/notifications/markdown/checkin-accessory.blade.php @@ -3,7 +3,7 @@ {{ trans('mail.the_following_item') }} -@if ($item->getImageUrl()) +@if (($snipeSettings->show_images_in_email =='1') && $item->getImageUrl())
Asset
@endif 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'))); + } +}