diff --git a/app/Http/Controllers/Admin/Data/FeatureController.php b/app/Http/Controllers/Admin/Data/FeatureController.php index 8d05e5797d..b0185c2f7e 100644 --- a/app/Http/Controllers/Admin/Data/FeatureController.php +++ b/app/Http/Controllers/Admin/Data/FeatureController.php @@ -164,7 +164,7 @@ public function postSortFeatureCategory(Request $request, FeatureService $servic */ public function getFeatureIndex(Request $request) { $query = Feature::query(); - $data = $request->only(['rarity_id', 'feature_category_id', 'species_id', 'subtype_id', 'name', 'sort', 'visibility']); + $data = $request->only(['rarity_id', 'feature_category_id', 'species_id', 'subtype_ids', 'name', 'sort', 'visibility']); if (isset($data['rarity_id']) && $data['rarity_id'] != 'none') { $query->where('rarity_id', $data['rarity_id']); } @@ -182,11 +182,13 @@ public function getFeatureIndex(Request $request) { $query->where('species_id', $data['species_id']); } } - if (isset($data['subtype_id']) && $data['subtype_id'] != 'none') { - if ($data['subtype_id'] == 'withoutOption') { - $query->whereNull('subtype_id'); + if (isset($data['subtype_ids']) && $data['subtype_ids']) { + if (!in_array('withoutOption', $data['subtype_ids'])) { + $query->whereJsonContains('subtype_ids', $data['subtype_ids']); } else { - $query->where('subtype_id', $data['subtype_id']); + $query->where(function ($query) use ($data) { + $query->whereNull('subtype_ids')->orWhereJsonLength('subtype_ids', 0); + }); } } if (isset($data['name'])) { @@ -238,7 +240,7 @@ public function getFeatureIndex(Request $request) { 'features' => $query->paginate(20)->appends($request->query()), 'rarities' => ['none' => 'Any Rarity'] + Rarity::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), 'specieses' => ['none' => 'Any Species'] + ['withoutOption' => 'Without Species'] + Species::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), - 'subtypes' => ['none' => 'Any Subtype'] + ['withoutOption' => 'Without Subtype'] + Subtype::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), + 'subtypes' => ['withoutOption' => 'Without Subtype'] + Subtype::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), 'categories' => ['none' => 'Any Category'] + ['withoutOption' => 'Without Category'] + FeatureCategory::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), ]); } @@ -275,7 +277,7 @@ public function getEditFeature($id) { 'feature' => $feature, 'rarities' => ['none' => 'Select a Rarity'] + Rarity::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), 'specieses' => ['none' => 'No restriction'] + Species::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), - 'subtypes' => ['none' => 'No subtype'] + Subtype::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), + 'subtypes' => Subtype::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), 'categories' => ['none' => 'No category'] + FeatureCategory::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), ]); } @@ -291,7 +293,7 @@ public function getEditFeature($id) { public function postCreateEditFeature(Request $request, FeatureService $service, $id = null) { $id ? $request->validate(Feature::$updateRules) : $request->validate(Feature::$createRules); $data = $request->only([ - 'name', 'species_id', 'subtype_id', 'rarity_id', 'feature_category_id', 'description', 'image', 'remove_image', 'is_visible', + 'name', 'species_id', 'subtype_ids', 'rarity_id', 'feature_category_id', 'description', 'image', 'remove_image', 'is_visible', ]); if ($id && $service->updateFeature(Feature::find($id), $data, Auth::user())) { flash('Trait updated successfully.')->success(); @@ -350,11 +352,11 @@ public function postDeleteFeature(Request $request, FeatureService $service, $id */ public function getCreateEditFeatureSubtype(Request $request) { $species = $request->input('species'); - $subtype_id = $request->input('subtype_id'); + $subtype_ids = $request->input('subtype_ids'); return view('admin.features._create_edit_feature_subtype', [ - 'subtypes' => ['0' => 'Select Subtype'] + Subtype::where('species_id', '=', $species)->orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), - 'subtype_id' => $subtype_id, + 'subtypes' => Subtype::where('species_id', '=', $species)->orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), + 'subtype_ids' => $subtype_ids, ]); } } diff --git a/app/Http/Controllers/WorldController.php b/app/Http/Controllers/WorldController.php index 18b3e408a9..6173b67a93 100644 --- a/app/Http/Controllers/WorldController.php +++ b/app/Http/Controllers/WorldController.php @@ -148,7 +148,7 @@ public function getFeatureCategories(Request $request) { */ public function getFeatures(Request $request) { $query = Feature::visible(Auth::user() ?? null)->with('category')->with('rarity')->with('species'); - $data = $request->only(['rarity_id', 'feature_category_id', 'species_id', 'subtype_id', 'name', 'sort']); + $data = $request->only(['rarity_id', 'feature_category_id', 'species_id', 'subtype_ids', 'name', 'sort']); if (isset($data['rarity_id']) && $data['rarity_id'] != 'none') { $query->where('rarity_id', $data['rarity_id']); } @@ -166,11 +166,13 @@ public function getFeatures(Request $request) { $query->where('species_id', $data['species_id']); } } - if (isset($data['subtype_id']) && $data['subtype_id'] != 'none') { - if ($data['subtype_id'] == 'withoutOption') { - $query->whereNull('subtype_id'); + if (isset($data['subtype_ids']) && $data['subtype_ids']) { + if (!in_array('withoutOption', $data['subtype_ids'])) { + $query->whereJsonContains('subtype_ids', $data['subtype_ids']); } else { - $query->where('subtype_id', $data['subtype_id']); + $query->where(function ($query) use ($data) { + $query->whereNull('subtype_ids')->orWhereJsonLength('subtype_ids', 0); + }); } } if (isset($data['name'])) { @@ -215,7 +217,7 @@ public function getFeatures(Request $request) { 'features' => $query->orderBy('id')->paginate(20)->appends($request->query()), 'rarities' => ['none' => 'Any Rarity'] + Rarity::orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), 'specieses' => ['none' => 'Any Species'] + ['withoutOption' => 'Without Species'] + Species::visible(Auth::user() ?? null)->orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), - 'subtypes' => ['none' => 'Any Subtype'] + ['withoutOption' => 'Without Subtype'] + Subtype::visible(Auth::user() ?? null)->orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), + 'subtypes' => ['withoutOption' => 'Without Subtype'] + Subtype::visible(Auth::user() ?? null)->orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), 'categories' => ['none' => 'Any Category'] + ['withoutOption' => 'Without Category'] + FeatureCategory::visible(Auth::user() ?? null)->orderBy('sort', 'DESC')->pluck('name', 'id')->toArray(), ]); } diff --git a/app/Models/Feature/Feature.php b/app/Models/Feature/Feature.php index b13dfc8729..a3e0060b23 100644 --- a/app/Models/Feature/Feature.php +++ b/app/Models/Feature/Feature.php @@ -15,7 +15,7 @@ class Feature extends Model { * @var array */ protected $fillable = [ - 'feature_category_id', 'species_id', 'subtype_id', 'rarity_id', 'name', 'has_image', 'description', 'parsed_description', 'is_visible', 'hash', + 'feature_category_id', 'species_id', 'subtype_ids', 'rarity_id', 'name', 'has_image', 'description', 'parsed_description', 'is_visible', 'hash', ]; /** @@ -24,6 +24,16 @@ class Feature extends Model { * @var string */ protected $table = 'features'; + + /** + * The attributes that should be cast to native types. + * + * @var array + */ + protected $casts = [ + 'subtype_ids' => 'array', + ]; + /** * Validation rules for creation. * @@ -32,7 +42,7 @@ class Feature extends Model { public static $createRules = [ 'feature_category_id' => 'nullable', 'species_id' => 'nullable', - 'subtype_id' => 'nullable', + 'subtype_ids' => 'nullable', 'rarity_id' => 'required|exists:rarities,id', 'name' => 'required|unique:features|between:3,100', 'description' => 'nullable', @@ -47,7 +57,7 @@ class Feature extends Model { public static $updateRules = [ 'feature_category_id' => 'nullable', 'species_id' => 'nullable', - 'subtype_id' => 'nullable', + 'subtype_ids' => 'nullable', 'rarity_id' => 'required|exists:rarities,id', 'name' => 'required|between:3,100', 'description' => 'nullable', @@ -74,13 +84,6 @@ public function species() { return $this->belongsTo(Species::class); } - /** - * Get the subtype the feature belongs to. - */ - public function subtype() { - return $this->belongsTo(Subtype::class); - } - /** * Get the category the feature belongs to. */ @@ -144,7 +147,7 @@ public function scopeSortSpecies($query) { public function scopeSortSubtype($query) { $ids = Subtype::orderBy('sort', 'DESC')->pluck('id')->toArray(); - return count($ids) ? $query->orderBy(DB::raw('FIELD(subtype_id, '.implode(',', $ids).')')) : $query; + return count($ids) ? $query->orderBy(DB::raw('FIELD(subtype_ids, '.implode(',', $ids).')')) : $query; } /** @@ -331,4 +334,16 @@ public static function getDropdownItems($withHidden = 0) { return self::where('is_visible', '>=', $visibleOnly)->orderBy('name')->pluck('name', 'id')->toArray(); } } + + /** + * Returns the subtype display for the feature. + */ + public function displaySubtypes() { + $result = []; + foreach ($this->subtype_ids as $id) { + $result[] = Subtype::find($id)->displayName; + } + + return implode(', ', $result); + } } diff --git a/app/Services/FeatureService.php b/app/Services/FeatureService.php index a7ede66b7c..b6348486ea 100644 --- a/app/Services/FeatureService.php +++ b/app/Services/FeatureService.php @@ -200,8 +200,8 @@ public function createFeature($data, $user) { if (isset($data['species_id']) && $data['species_id'] == 'none') { $data['species_id'] = null; } - if (isset($data['subtype_id']) && $data['subtype_id'] == 'none') { - $data['subtype_id'] = null; + if (!isset($data['subtype_ids']) || !$data['subtype_ids']) { + $data['subtype_ids'] = null; } if ((isset($data['feature_category_id']) && $data['feature_category_id']) && !FeatureCategory::where('id', $data['feature_category_id'])->exists()) { @@ -210,13 +210,15 @@ public function createFeature($data, $user) { if ((isset($data['species_id']) && $data['species_id']) && !Species::where('id', $data['species_id'])->exists()) { throw new \Exception('The selected species is invalid.'); } - if (isset($data['subtype_id']) && $data['subtype_id']) { - $subtype = Subtype::find($data['subtype_id']); - if (!(isset($data['species_id']) && $data['species_id'])) { - throw new \Exception('Species must be selected to select a subtype.'); - } - if (!$subtype || $subtype->species_id != $data['species_id']) { - throw new \Exception('Selected subtype invalid or does not match species.'); + if (isset($data['subtype_ids']) && $data['subtype_ids']) { + foreach ($data['subtype_ids'] as $subtype_id) { + $subtype = Subtype::find($data['subtype_id']); + if (!(isset($data['species_id']) && $data['species_id'])) { + throw new \Exception('Species must be selected to select a subtype.'); + } + if (!$subtype || $subtype->species_id != $data['species_id']) { + throw new \Exception('Selected subtype invalid or does not match species.'); + } } } @@ -269,8 +271,8 @@ public function updateFeature($feature, $data, $user) { if (isset($data['species_id']) && $data['species_id'] == 'none') { $data['species_id'] = null; } - if (isset($data['subtype_id']) && $data['subtype_id'] == 'none') { - $data['subtype_id'] = null; + if (!isset($data['subtype_ids']) || !$data['subtype_ids']) { + $data['subtype_ids'] = null; } // More specific validation @@ -283,13 +285,16 @@ public function updateFeature($feature, $data, $user) { if ((isset($data['species_id']) && $data['species_id']) && !Species::where('id', $data['species_id'])->exists()) { throw new \Exception('The selected species is invalid.'); } - if (isset($data['subtype_id']) && $data['subtype_id']) { - $subtype = Subtype::find($data['subtype_id']); - if (!(isset($data['species_id']) && $data['species_id'])) { - throw new \Exception('Species must be selected to select a subtype.'); - } - if (!$subtype || $subtype->species_id != $data['species_id']) { - throw new \Exception('Selected subtype invalid or does not match species.'); + + if (isset($data['subtype_ids']) && $data['subtype_ids']) { + foreach ($data['subtype_ids'] as $subtype_id) { + $subtype = Subtype::find($subtype_id); + if (!(isset($data['species_id']) && $data['species_id'])) { + throw new \Exception('Species must be selected to select a subtype.'); + } + if (!$subtype || $subtype->species_id != $data['species_id']) { + throw new \Exception('Selected subtype invalid or does not match species.'); + } } } diff --git a/database/migrations/2024_10_08_174651_make_feature_subtype_id_allow_multiple.php b/database/migrations/2024_10_08_174651_make_feature_subtype_id_allow_multiple.php new file mode 100644 index 0000000000..e82ba4d0e7 --- /dev/null +++ b/database/migrations/2024_10_08_174651_make_feature_subtype_id_allow_multiple.php @@ -0,0 +1,32 @@ +dropColumn('subtype_id'); + $table->json('subtype_ids')->nullable()->default(null); + }); + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + Schema::table('features', function (Blueprint $table) { + $table->dropColumn('subtype_ids'); + $table->integer('subtype_id')->nullable()->default(null); + }); + } +}; diff --git a/resources/views/admin/features/_create_edit_feature_subtype.blade.php b/resources/views/admin/features/_create_edit_feature_subtype.blade.php index 2f571e001f..555de85dda 100644 --- a/resources/views/admin/features/_create_edit_feature_subtype.blade.php +++ b/resources/views/admin/features/_create_edit_feature_subtype.blade.php @@ -1,2 +1,2 @@ -{!! Form::label('Subtype (Optional)') !!} {!! add_help('This is cosmetic and does not limit choice of traits in selections.') !!} -{!! Form::select('subtype_id', $subtypes, $subtype_id, ['class' => 'form-control', 'id' => 'subtype']) !!} +{!! Form::label('Subtypes (Optional)') !!} {!! add_help('This is cosmetic and does not limit choice of traits in selections.') !!} +{!! Form::select('subtype_ids[]', $subtypes, $subtype_ids, ['class' => 'form-control selectize', 'id' => 'subtype', 'multiple', 'placeholder' => 'Select Subtypes']) !!} diff --git a/resources/views/admin/features/create_edit_feature.blade.php b/resources/views/admin/features/create_edit_feature.blade.php index c6aec2f2d7..ad495c489f 100644 --- a/resources/views/admin/features/create_edit_feature.blade.php +++ b/resources/views/admin/features/create_edit_feature.blade.php @@ -50,8 +50,8 @@ {!! Form::select('species_id', $specieses, $feature->species_id, ['class' => 'form-control', 'id' => 'species']) !!}
- {!! Form::label('Subtype (Optional)') !!} {!! add_help('This is cosmetic and does not limit choice of traits in selections.') !!} - {!! Form::select('subtype_id', $subtypes, $feature->subtype_id, ['class' => 'form-control', 'id' => 'subtype']) !!} + {!! Form::label('Subtypes (Optional)') !!} {!! add_help('This is cosmetic and does not limit choice of traits in selections.') !!} + {!! Form::select('subtype_ids[]', $subtypes, $feature->subtype_ids, ['class' => 'form-control selectize', 'id' => 'subtype', 'multiple', 'placeholder' => 'Select a Species']) !!}
@@ -84,6 +84,8 @@ @parent @endsection diff --git a/resources/views/world/_feature_entry.blade.php b/resources/views/world/_feature_entry.blade.php index 71eee4909a..6e9d17cb8b 100644 --- a/resources/views/world/_feature_entry.blade.php +++ b/resources/views/world/_feature_entry.blade.php @@ -25,8 +25,8 @@ @if ($feature->species_id)
Species: {!! $feature->species->displayName !!} - @if ($feature->subtype_id) - ({!! $feature->subtype->displayName !!} subtype) + @if ($feature->subtype_ids) + ({!! $feature->displaySubtypes() !!} subtype{{ count($feature->subtype_ids) > 1 ? 's' : '' }}) @endif
@endif diff --git a/resources/views/world/features.blade.php b/resources/views/world/features.blade.php index 1af43478c5..8228948df9 100644 --- a/resources/views/world/features.blade.php +++ b/resources/views/world/features.blade.php @@ -17,8 +17,8 @@
{!! Form::select('species_id', $specieses, Request::get('species_id'), ['class' => 'form-control']) !!}
-
- {!! Form::select('subtype_id', $subtypes, Request::get('subtype_id'), ['class' => 'form-control']) !!} +
+ {!! Form::select('subtype_ids[]', $subtypes, Request::get('subtype_ids'), ['class' => 'form-control selectize', 'multiple', 'placeholder' => 'Any Subtype']) !!}
{!! Form::select('rarity_id', $rarities, Request::get('rarity_id'), ['class' => 'form-control']) !!} @@ -65,3 +65,10 @@
{{ $features->total() }} result{{ $features->total() == 1 ? '' : 's' }} found.
@endsection +@section('scripts') + +@endsection diff --git a/tests/Browser/console/Tests_Browser_LoginTest_testUserCanLogin-0.log b/tests/Browser/console/Tests_Browser_LoginTest_testUserCanLogin-0.log new file mode 100644 index 0000000000..f95b19309f --- /dev/null +++ b/tests/Browser/console/Tests_Browser_LoginTest_testUserCanLogin-0.log @@ -0,0 +1,14 @@ +[ + { + "level": "SEVERE", + "message": "http:\/\/localhost:8000\/login - Failed to load resource: the server responded with a status of 500 (Internal Server Error)", + "source": "network", + "timestamp": 1727186924675 + }, + { + "level": "SEVERE", + "message": "http:\/\/localhost:8000\/login 302:8 Uncaught SyntaxError: Identifier 'Rt' has already been declared", + "source": "javascript", + "timestamp": 1727186925377 + } +] \ No newline at end of file diff --git a/tests/Browser/screenshots/failure-Tests_Browser_LoginTest_testUserCanLogin-0.png b/tests/Browser/screenshots/failure-Tests_Browser_LoginTest_testUserCanLogin-0.png new file mode 100644 index 0000000000..1e1bfec2f5 Binary files /dev/null and b/tests/Browser/screenshots/failure-Tests_Browser_LoginTest_testUserCanLogin-0.png differ