From 5f916c71ccd9e94e7675bcb52406d1bc6219f87d Mon Sep 17 00:00:00 2001 From: Spitfire Date: Wed, 5 Feb 2025 17:26:11 -0600 Subject: [PATCH 1/6] New filters --- app/Datagrids/Filters/CharacterFilter.php | 33 +--- app/Datagrids/Filters/DatagridFilter.php | 101 +++++++++- app/Models/Character.php | 4 + app/Models/Concerns/HasFilters.php | 183 +++++++++++++++++- app/Services/FilterService.php | 32 +++ .../views/components/forms/select.blade.php | 17 +- .../cruds/datagrids/filters/_array.blade.php | 39 ++-- resources/views/filters/form.blade.php | 16 +- 8 files changed, 364 insertions(+), 61 deletions(-) diff --git a/app/Datagrids/Filters/CharacterFilter.php b/app/Datagrids/Filters/CharacterFilter.php index ee1f541b87..8f96cb795d 100644 --- a/app/Datagrids/Filters/CharacterFilter.php +++ b/app/Datagrids/Filters/CharacterFilter.php @@ -33,35 +33,10 @@ public function build() $this ->add('name') ->add('title') - ->add([ - 'field' => 'family_id', - 'label' => Module::singular(config('entities.ids.family'), __('entities.family')), - 'type' => 'select2', - 'route' => route('search-list', [$this->campaign, config('entities.ids.family')]), - 'placeholder' => $famPlaceholder, - 'model' => Family::class, - 'withChildren' => true, - ]) - ->location() - ->add([ - 'field' => 'race_id', - 'label' => Module::singular(config('entities.ids.race'), __('entities.race')), - 'type' => 'select2', - 'route' => route('search-list', [$this->campaign, config('entities.ids.race')]), - 'placeholder' => $racePlaceholder, - 'model' => Race::class, - 'withChildren' => true, - ]) - ->add([ - 'field' => 'member_id', - 'label' => Module::singular(config('entities.ids.organisation'), __('entities.organisation')), - 'type' => 'select2', - 'route' => route('search-list', [$this->campaign, config('entities.ids.organisation')]), - 'placeholder' => $orgPlaceholder, - 'model' => Organisation::class, - 'withChildren' => true, - - ]) + ->families() + ->locations() + ->races() + ->organisations() ->add('type') ->add('age') ->add('sex') diff --git a/app/Datagrids/Filters/DatagridFilter.php b/app/Datagrids/Filters/DatagridFilter.php index 02ad81b323..2cc75a506e 100644 --- a/app/Datagrids/Filters/DatagridFilter.php +++ b/app/Datagrids/Filters/DatagridFilter.php @@ -4,8 +4,11 @@ use App\Facades\Module; use App\Models\Character; +use App\Models\Family; use App\Models\Journal; use App\Models\Location; +use App\Models\Organisation; +use App\Models\Race; use App\Models\Tag; use App\Traits\CampaignAware; use Illuminate\Support\Facades\Auth; @@ -68,6 +71,102 @@ protected function location(): self return $this; } + /** + * Add the locations filters + * @return $this + */ + protected function locations(): self + { + $name = Module::plural(config('entities.ids.location')); + $placeholder = __('crud.placeholders.multiple'); + if (!empty($name)) { + $placeholder = __('crud.placeholders.fallback', ['module' => $name]); + } + $this->filters[] = [ + 'field' => 'locations', + 'label' => Module::plural(config('entities.ids.location'), __('entities.locations')), + 'type' => 'selectMultiple', + 'route' => route('search-list', [$this->campaign, config('entities.ids.location')]), + 'placeholder' => $placeholder, + 'model' => Location::class, + 'multiple' => true, + 'withChildren' => true, + ]; + return $this; + } + + /** + * Add the races filters + * @return $this + */ + protected function races(): self + { + $name = Module::plural(config('entities.ids.race')); + $placeholder = __('crud.placeholders.multiple'); + if (!empty($name)) { + $placeholder = __('crud.placeholders.multiple', ['module' => $name]); + } + $this->filters[] = [ + 'field' => 'races', + 'label' => Module::plural(config('entities.ids.race'), __('entities.races')), + 'type' => 'selectMultiple', + 'route' => route('search-list', [$this->campaign, config('entities.ids.race')]), + 'placeholder' => $placeholder, + 'model' => Race::class, + 'multiple' => true, + 'withChildren' => true, + ]; + return $this; + } + + /** + * Add the organisations filters + * @return $this + */ + protected function organisations(): self + { + $name = Module::plural(config('entities.ids.organisation')); + $placeholder = __('crud.placeholders.multiple'); + if (!empty($name)) { + $placeholder = __('crud.placeholders.fallback', ['module' => $name]); + } + $this->filters[] = [ + 'field' => 'organisations', + 'label' => Module::plural(config('entities.ids.organisation'), __('entities.organisations')), + 'type' => 'selectMultiple', + 'route' => route('search-list', [$this->campaign, config('entities.ids.organisation')]), + 'placeholder' => $placeholder, + 'model' => Organisation::class, + 'multiple' => true, + 'withChildren' => true, + ]; + return $this; + } + + /** + * Add the families filters + * @return $this + */ + protected function families(): self + { + $name = Module::plural(config('entities.ids.family')); + $placeholder = __('crud.placeholders.multiple'); + if (!empty($name)) { + $placeholder = __('crud.placeholders.fallback', ['module' => $name]); + } + $this->filters[] = [ + 'field' => 'families', + 'label' => Module::plural(config('entities.ids.family'), __('entities.families')), + 'type' => 'selectMultiple', + 'route' => route('search-list', [$this->campaign, config('entities.ids.family')]), + 'placeholder' => $placeholder, + 'model' => Family::class, + 'multiple' => true, + 'withChildren' => true, + ]; + return $this; + } + /** * Add the character filters * @return $this @@ -125,7 +224,7 @@ protected function tags(): self } $this->filters[] = [ 'field' => 'tags', - 'label' => Module::singular(config('entities.ids.tag'), __('entities.tag')), + 'label' => Module::singular(config('entities.ids.tag'), __('entities.tags')), 'type' => 'tag', 'route' => route('search-list', [$this->campaign, config('entities.ids.tag')]), 'placeholder' => $placeholder, diff --git a/app/Models/Character.php b/app/Models/Character.php index 60b9a20dad..3755210720 100644 --- a/app/Models/Character.php +++ b/app/Models/Character.php @@ -439,6 +439,10 @@ public function filterableColumns(): array 'sex', 'pronouns', 'location_id', + 'locations', + 'organisations', + 'races', + 'families', 'is_dead', 'member_id', 'race_id', diff --git a/app/Models/Concerns/HasFilters.php b/app/Models/Concerns/HasFilters.php index 51bd6e3573..390591b174 100644 --- a/app/Models/Concerns/HasFilters.php +++ b/app/Models/Concerns/HasFilters.php @@ -9,6 +9,7 @@ use App\Models\Location; use App\Models\Family; use App\Models\Race; +use Illuminate\Support\Facades\DB; /** * HasFilters @@ -84,8 +85,8 @@ public function scopeFilter(Builder $query, array $params = []): Builder foreach ($this->filterParams as $key => $value) { if (isset($value) && in_array($key, $fields)) { - // The requested field is an array, which we don't support for anything other than tags ("or" searches) - if (is_array($value) && $key != "tags") { + // The requested field is an array, which we don't support for anything other than tags, and locations ("or" searches) + if (is_array($value) && $key != "tags" && $key != "locations" && $key != "organisations" && $key != "races" && $key != "families") { continue; } $this->filterOption = !empty($params[$key . '_option']) ? $params[$key . '_option'] : null; @@ -111,6 +112,14 @@ public function scopeFilter(Builder $query, array $params = []): Builder if ($key === 'tags') { $this->filterTags($query, $value); + } elseif ($key == 'locations') { + $this->filterMultipleLocations($query, $value); + } elseif ($key == 'organisations') { + $this->filterOrganisations($query, $value); + } elseif ($key == 'races') { + $this->filterRaces($query, $value); + } elseif ($key == 'families') { + $this->filterFamilies($query, $value); } elseif (in_array($key, ['date_start' , 'date_end'])) { $this->filterDateRange($query, $key, $params); } elseif ($key == 'races') { @@ -179,7 +188,7 @@ protected function extractSearchOperator($value, string $key): void { $operator = 'like'; $filterValue = $value; - if (($key !== 'tags') && ($key !== 'races')) { + if (($key !== 'tags') && ($key !== 'races') && ($key !== 'locations') && ($key !== 'families') && ($key !== 'organisations')) { if ($value == '!!') { $operator = 'IS NULL'; $filterValue = null; @@ -475,17 +484,34 @@ protected function filterRaces(Builder $query, null|string|array $value = null): if (!is_array($value)) { $value = [$value]; } + $raceIds = []; + foreach ($value as $v) { + $raceIds[] = (int) $v; + } if ($this->filterOption('exclude')) { - $raceIds = []; - foreach ($value as $v) { - $raceIds[] = (int) $v; - } $query->whereRaw('(select count(*) from character_race as cr where cr.character_id = ' . $this->getTable() . '.id and cr.race_id in (' . implode(', ', $raceIds) . ')) = 0'); return; } + if ($this->filterOption('children')) { + $races = DB::select(" + WITH RECURSIVE race_tree AS ( + -- Select parents + SELECT * FROM races WHERE id IN (".implode(',', $raceIds).") + UNION ALL + -- Recursively get all descendants + SELECT r.* FROM races r + INNER JOIN race_tree rt ON r.race_id = rt.id + ) + SELECT * FROM race_tree + "); + $raceIds = collect($races)->pluck('id'); // Extract IDs + $query->whereIn($this->getTable() . '.race_id', $raceIds)->distinct(); + return; + } + foreach ($value as $v) { $v = (int) $v; $query @@ -519,6 +545,144 @@ protected function filterLocations(Builder $query, string $value = null, string $this->filterFallback($query, $key); } + /** + * Filter on characters on multiple locations + */ + protected function filterMultipleLocations(Builder $query, null|string|array $value = null, string $key = null): void + { + // "none" filter keys is handled later + if ($this->filterOption('none')) { + return; + } + + $query + ->joinEntity() + ; + // Make sure we always have an array + if (!is_array($value)) { + $value = [$value]; + } + $locationIds = []; + foreach ($value as $v) { + $locationIds[] = (int) $v; + } + + if ($this->filterOption('exclude')) { + $query->whereNotIn($this->getTable() . '.location_id', $locationIds)->distinct(); + return; + } + + if ($this->filterOption('children')) { + $locations = DB::select(" + WITH RECURSIVE location_tree AS ( + -- Select parents + SELECT * FROM locations WHERE id IN (".implode(',', $locationIds).") + UNION ALL + -- Recursively get all descendants + SELECT l.* FROM locations l + INNER JOIN location_tree lt ON l.location_id = lt.id + ) + SELECT * FROM location_tree + "); + $locIds = collect($locations)->pluck('id'); // Extract IDs + $query->whereIn($this->getTable() . '.location_id', $locIds)->distinct(); + return; + } + + $query->whereIn($this->getTable() . '.location_id', $locationIds)->distinct(); + } + + /** + * Filter on characters on multiple organisations + */ + protected function filterOrganisations(Builder $query, null|string|array $value = null, string $key = null): void + { + // "none" filter keys is handled later + if ($this->filterOption('none')) { + return; + } + $query + ->joinEntity() + ; + + // Make sure we always have an array + if (!is_array($value)) { + $value = [$value]; + } + $orgIds = []; + foreach ($value as $v) { + $orgIds[] = (int) $v; + } + + if ($this->filterOption('exclude')) { + $query->whereRaw('(select count(*) from organisation_member as cr where cr.character_id = ' . + $this->getTable() . '.id and cr.organisation_id in (' . implode(', ', $orgIds) . ')) = 0'); + return; + } + + if ($this->filterOption('children')) { + $orgs = DB::select(" + WITH RECURSIVE organisation_tree AS ( + -- Select parents + SELECT * FROM organisations WHERE id IN (".implode(',', $orgIds).") + UNION ALL + -- Recursively get all descendants + SELECT r.* FROM organisations r + INNER JOIN organisation_tree rt ON r.organisation_id = rt.id + ) + SELECT * FROM organisation_tree + "); + $orgIds = collect($orgs)->pluck('id'); // Extract IDs + $query->whereIn($this->getTable() . '.organisation_id', $orgIds)->distinct(); + return; + } + + foreach ($value as $v) { + $v = (int) $v; + $query + ->leftJoin('organisation_member as cr' . $v, "cr{$v}.character_id", $this->getTable() . '.id') + ->where("cr{$v}.organisation_id", $v) + ; + } + } + + /** + * Filter on characters on multiple families + */ + protected function filterFamilies(Builder $query, null|string|array $value = null, string $key = null): void + { + // "none" filter keys is handled later + if ($this->filterOption('none')) { + return; + } + $query + ->joinEntity() + ; + + // Make sure we always have an array + if (!is_array($value)) { + $value = [$value]; + } + + if ($this->filterOption('exclude')) { + $familyIds = []; + foreach ($value as $v) { + $familyIds[] = (int) $v; + } + $query->whereRaw('(select count(*) from character_family as cr where cr.character_id = ' . + $this->getTable() . '.id and cr.family_id in (' . implode(', ', $familyIds) . ')) = 0'); + return; + } + + foreach ($value as $v) { + $v = (int) $v; + $query + ->leftJoin('character_family as cr' . $v, "cr{$v}.character_id", $this->getTable() . '.id') + ->where("cr{$v}.family_id", $v) + ; + } + } + /** * Filter characters on a single race */ @@ -670,6 +834,11 @@ protected function filterMember(Builder $query, ?string $value = null): void protected function filterNoneOptions(Builder $query, string $key, array $fields = []): void { $key = Str::beforeLast($key, '_option'); + + if (in_array($key, ['races', 'families', 'locations', 'organisations'])) { + $names = ['races' => 'race_id', 'families' => 'family_id', 'organisations' => 'organisation_id', 'locations' => 'location_id']; + $key = $names[$key]; + } // Validate the key is a filter if (!in_array($key, $fields)) { return; diff --git a/app/Services/FilterService.php b/app/Services/FilterService.php index d0424be7dc..47708dabad 100644 --- a/app/Services/FilterService.php +++ b/app/Services/FilterService.php @@ -135,6 +135,38 @@ protected function prepareFilters(array $availableFilters = []): self unset($this->data['tags']); } } + // If we have data but no "locations" array, it's empty + if (!empty($this->data) && in_array('locations', $availableFilters) && !isset($this->data['locations'])) { + // Not calling from a page or order result, we can junk the filters + if (empty($this->data['page']) && empty($this->data['order'])) { + unset($this->data['locations']); + } + } + + // If we have data but no "organisations" array, it's empty + if (!empty($this->data) && in_array('organisations', $availableFilters) && !isset($this->data['organisations'])) { + // Not calling from a page or order result, we can junk the filters + if (empty($this->data['page']) && empty($this->data['order'])) { + unset($this->data['organisations']); + } + } + + // If we have data but no "races" array, it's empty + if (!empty($this->data) && in_array('races', $availableFilters) && !isset($this->data['races'])) { + // Not calling from a page or order result, we can junk the filters + if (empty($this->data['page']) && empty($this->data['order'])) { + unset($this->data['races']); + } + } + + // If we have data but no "families" array, it's empty + if (!empty($this->data) && in_array('families', $availableFilters) && !isset($this->data['families'])) { + // Not calling from a page or order result, we can junk the filters + if (empty($this->data['page']) && empty($this->data['order'])) { + unset($this->data['families']); + } + } + // Don't support the old tags, force using tags[] if (isset($this->data['tags']) && !is_array($this->data['tags'])) { unset($this->data['tags']); diff --git a/resources/views/components/forms/select.blade.php b/resources/views/components/forms/select.blade.php index c6f4d1e353..4f89db8b84 100644 --- a/resources/views/components/forms/select.blade.php +++ b/resources/views/components/forms/select.blade.php @@ -16,16 +16,17 @@ class="w-full {{ $class }}" @if ($multiple) multiple @endif @if (!empty($label)) aria-label="{!! $label !!}" @endif @foreach ($extra as $k => $v) {{ $k }}="{{ $v }}" @endforeach ->@foreach ($options as $k => $v) - @if (is_array($v)) - - @foreach ($v as $k2 => $v2) +> +@foreach ($options as $k => $v) + @if (is_array($v)) + + @foreach ($v as $k2 => $v2) - @endforeach - - @else + @endforeach + + @else @endif @endforeach - @endif +@endif diff --git a/resources/views/cruds/datagrids/filters/_array.blade.php b/resources/views/cruds/datagrids/filters/_array.blade.php index ea23566f1e..c457877e5a 100644 --- a/resources/views/cruds/datagrids/filters/_array.blade.php +++ b/resources/views/cruds/datagrids/filters/_array.blade.php @@ -1,10 +1,31 @@ + +@php + $options = [ + '' => __('crud.filters.options.include'), + 'children' => __('crud.filters.options.children'), + 'exclude' => __('crud.filters.options.exclude'), + 'none' => __('crud.filters.options.none'), + ]; + if (!isset($field['withChildren']) || $field['withChildren'] !== true) { + unset($options['children']); + } + if ($field['type'] == 'selectMultiple' && isset($models)) { + $selectedModels = $models; + $ids = array_keys($selectedModels); + } elseif (!empty($model) ) { + $selectedModels = [$model->id => $model->name]; + } else { + $selectedModels = []; + } +@endphp
- @php - $options = [ - '' => __('crud.filters.options.include'), - 'children' => __('crud.filters.options.children'), - 'exclude' => __('crud.filters.options.exclude'), - 'none' => __('crud.filters.options.none'), - ]; - if (!isset($field['withChildren']) || $field['withChildren'] !== true) { - unset($options['children']); - } - @endphp + @if (is_array($field)) - single($field['field']); if (!empty($value) && $field['type'] == 'select2') { $modelclass = new $field['model']; $model = $modelclass->find($value); } - ?> + // Support for fields with multiple models selected + if (Illuminate\Support\Arr::get($field, 'multiple') === true && $field['type'] == 'selectMultiple') { + $value = $filterService->filterValue($field['field']); + if (!empty($value)) { + $modelclass = new $field['model']; + $models = $modelclass->whereIn('id', $value)->get()->pluck('name', 'id')->toArray(); + } else { + unset($models); + } + } + ?> + @if ($field['type'] === 'tag') @include('cruds.datagrids.filters._tag', ['value' => $filterService->filterValue('tags')]) @elseif ($field['type'] === 'select') From bb7b7fbb78e7289c04eb59596ac52dc3369986c3 Mon Sep 17 00:00:00 2001 From: spitfire305 Date: Wed, 5 Feb 2025 23:27:45 +0000 Subject: [PATCH 2/6] Fix styling --- app/Datagrids/Filters/CharacterFilter.php | 3 --- app/Models/Concerns/HasFilters.php | 24 +++++++++++------------ app/Services/MentionsService.php | 2 +- 3 files changed, 13 insertions(+), 16 deletions(-) diff --git a/app/Datagrids/Filters/CharacterFilter.php b/app/Datagrids/Filters/CharacterFilter.php index 8f96cb795d..1d887ffa1e 100644 --- a/app/Datagrids/Filters/CharacterFilter.php +++ b/app/Datagrids/Filters/CharacterFilter.php @@ -3,9 +3,6 @@ namespace App\Datagrids\Filters; use App\Facades\Module; -use App\Models\Family; -use App\Models\Organisation; -use App\Models\Race; class CharacterFilter extends DatagridFilter { diff --git a/app/Models/Concerns/HasFilters.php b/app/Models/Concerns/HasFilters.php index 390591b174..67f9e73826 100644 --- a/app/Models/Concerns/HasFilters.php +++ b/app/Models/Concerns/HasFilters.php @@ -499,7 +499,7 @@ protected function filterRaces(Builder $query, null|string|array $value = null): $races = DB::select(" WITH RECURSIVE race_tree AS ( -- Select parents - SELECT * FROM races WHERE id IN (".implode(',', $raceIds).") + SELECT * FROM races WHERE id IN (" . implode(',', $raceIds) . ") UNION ALL -- Recursively get all descendants SELECT r.* FROM races r @@ -507,10 +507,10 @@ protected function filterRaces(Builder $query, null|string|array $value = null): ) SELECT * FROM race_tree "); - $raceIds = collect($races)->pluck('id'); // Extract IDs - $query->whereIn($this->getTable() . '.race_id', $raceIds)->distinct(); - return; - } + $raceIds = collect($races)->pluck('id'); // Extract IDs + $query->whereIn($this->getTable() . '.race_id', $raceIds)->distinct(); + return; + } foreach ($value as $v) { $v = (int) $v; @@ -573,10 +573,10 @@ protected function filterMultipleLocations(Builder $query, null|string|array $va } if ($this->filterOption('children')) { - $locations = DB::select(" + $locations = DB::select(" WITH RECURSIVE location_tree AS ( -- Select parents - SELECT * FROM locations WHERE id IN (".implode(',', $locationIds).") + SELECT * FROM locations WHERE id IN (" . implode(',', $locationIds) . ") UNION ALL -- Recursively get all descendants SELECT l.* FROM locations l @@ -624,7 +624,7 @@ protected function filterOrganisations(Builder $query, null|string|array $value $orgs = DB::select(" WITH RECURSIVE organisation_tree AS ( -- Select parents - SELECT * FROM organisations WHERE id IN (".implode(',', $orgIds).") + SELECT * FROM organisations WHERE id IN (" . implode(',', $orgIds) . ") UNION ALL -- Recursively get all descendants SELECT r.* FROM organisations r @@ -632,10 +632,10 @@ protected function filterOrganisations(Builder $query, null|string|array $value ) SELECT * FROM organisation_tree "); - $orgIds = collect($orgs)->pluck('id'); // Extract IDs - $query->whereIn($this->getTable() . '.organisation_id', $orgIds)->distinct(); - return; - } + $orgIds = collect($orgs)->pluck('id'); // Extract IDs + $query->whereIn($this->getTable() . '.organisation_id', $orgIds)->distinct(); + return; + } foreach ($value as $v) { $v = (int) $v; diff --git a/app/Services/MentionsService.php b/app/Services/MentionsService.php index 4e005f3517..2c69eef7d0 100644 --- a/app/Services/MentionsService.php +++ b/app/Services/MentionsService.php @@ -432,7 +432,7 @@ protected function replaceEntityMentions(): void . ''; } elseif ($field === 'attributes') { return ''; - } elseif (!$entity->isMissingChild() && isset($entity->child->$field)) { + } elseif (!$entity->isMissingChild() && isset($entity->child->$field)) { $foreign = $entity->child->$field; if ($foreign instanceof Model) { if (isset($foreign->name) && !empty($foreign->name)) { From 26ddbad3f33eef57d316d0157e811ea48b3657d5 Mon Sep 17 00:00:00 2001 From: Spitfire Date: Thu, 6 Feb 2025 09:38:08 -0600 Subject: [PATCH 3/6] Implemented requested changes --- app/Datagrids/Filters/DatagridFilter.php | 2 +- app/Models/Concerns/HasFilters.php | 101 +++++++++++------------ app/Services/FilterService.php | 15 ++++ resources/views/filters/form.blade.php | 6 +- 4 files changed, 66 insertions(+), 58 deletions(-) diff --git a/app/Datagrids/Filters/DatagridFilter.php b/app/Datagrids/Filters/DatagridFilter.php index 2cc75a506e..da212ff86a 100644 --- a/app/Datagrids/Filters/DatagridFilter.php +++ b/app/Datagrids/Filters/DatagridFilter.php @@ -224,7 +224,7 @@ protected function tags(): self } $this->filters[] = [ 'field' => 'tags', - 'label' => Module::singular(config('entities.ids.tag'), __('entities.tags')), + 'label' => Module::plural(config('entities.ids.tag'), __('entities.tags')), 'type' => 'tag', 'route' => route('search-list', [$this->campaign, config('entities.ids.tag')]), 'placeholder' => $placeholder, diff --git a/app/Models/Concerns/HasFilters.php b/app/Models/Concerns/HasFilters.php index 67f9e73826..cc557df71d 100644 --- a/app/Models/Concerns/HasFilters.php +++ b/app/Models/Concerns/HasFilters.php @@ -8,6 +8,7 @@ use Illuminate\Support\Str; use App\Models\Location; use App\Models\Family; +use App\Models\Organisation; use App\Models\Race; use Illuminate\Support\Facades\DB; @@ -86,7 +87,7 @@ public function scopeFilter(Builder $query, array $params = []): Builder foreach ($this->filterParams as $key => $value) { if (isset($value) && in_array($key, $fields)) { // The requested field is an array, which we don't support for anything other than tags, and locations ("or" searches) - if (is_array($value) && $key != "tags" && $key != "locations" && $key != "organisations" && $key != "races" && $key != "families") { + if (is_array($value) && !in_array($key, ['tags', 'locations', 'organisations', 'races', 'families'])) { continue; } $this->filterOption = !empty($params[$key . '_option']) ? $params[$key . '_option'] : null; @@ -113,7 +114,7 @@ public function scopeFilter(Builder $query, array $params = []): Builder if ($key === 'tags') { $this->filterTags($query, $value); } elseif ($key == 'locations') { - $this->filterMultipleLocations($query, $value); + $this->filterLocations($query, $value); } elseif ($key == 'organisations') { $this->filterOrganisations($query, $value); } elseif ($key == 'races') { @@ -125,7 +126,7 @@ public function scopeFilter(Builder $query, array $params = []): Builder } elseif ($key == 'races') { $this->filterRaces($query, $value); } elseif ($key == 'location_id') { - $this->filterLocations($query, $value, $key); + $this->filterLocation($query, $value, $key); } elseif ($key == 'tag_id') { $query ->joinEntity() @@ -188,7 +189,7 @@ protected function extractSearchOperator($value, string $key): void { $operator = 'like'; $filterValue = $value; - if (($key !== 'tags') && ($key !== 'races') && ($key !== 'locations') && ($key !== 'families') && ($key !== 'organisations')) { + if (!in_array($key, ['tags', 'locations', 'organisations', 'races', 'families'])) { if ($value == '!!') { $operator = 'IS NULL'; $filterValue = null; @@ -496,20 +497,14 @@ protected function filterRaces(Builder $query, null|string|array $value = null): } if ($this->filterOption('children')) { - $races = DB::select(" - WITH RECURSIVE race_tree AS ( - -- Select parents - SELECT * FROM races WHERE id IN (" . implode(',', $raceIds) . ") - UNION ALL - -- Recursively get all descendants - SELECT r.* FROM races r - INNER JOIN race_tree rt ON r.race_id = rt.id - ) - SELECT * FROM race_tree - "); - $raceIds = collect($races)->pluck('id'); // Extract IDs - $query->whereIn($this->getTable() . '.race_id', $raceIds)->distinct(); - return; + $ids = []; + $parents = Race::whereIn('id', $raceIds)->with('descendants')->get(); + foreach ($parents as $parent) { + $childIds = $parent->descendants->pluck('id')->toArray(); + array_push($childIds, $parent->id); + $ids = $childIds + $ids; + } + $value = $ids; } foreach ($value as $v) { @@ -522,9 +517,9 @@ protected function filterRaces(Builder $query, null|string|array $value = null): } /** - * Filter on characters on multiple locations + * Filter characters in location */ - protected function filterLocations(Builder $query, string $value = null, string $key = null): void + protected function filterLocation(Builder $query, string $value = null, string $key = null): void { if (method_exists($this, 'scopeLocation')) { $query->location($value, $this->getFilterOption()); @@ -548,7 +543,7 @@ protected function filterLocations(Builder $query, string $value = null, string /** * Filter on characters on multiple locations */ - protected function filterMultipleLocations(Builder $query, null|string|array $value = null, string $key = null): void + protected function filterLocations(Builder $query, null|string|array $value = null, string $key = null): void { // "none" filter keys is handled later if ($this->filterOption('none')) { @@ -573,20 +568,14 @@ protected function filterMultipleLocations(Builder $query, null|string|array $va } if ($this->filterOption('children')) { - $locations = DB::select(" - WITH RECURSIVE location_tree AS ( - -- Select parents - SELECT * FROM locations WHERE id IN (" . implode(',', $locationIds) . ") - UNION ALL - -- Recursively get all descendants - SELECT l.* FROM locations l - INNER JOIN location_tree lt ON l.location_id = lt.id - ) - SELECT * FROM location_tree - "); - $locIds = collect($locations)->pluck('id'); // Extract IDs - $query->whereIn($this->getTable() . '.location_id', $locIds)->distinct(); - return; + $ids = []; + $parents = Location::whereIn('id', $locationIds)->with('descendants')->get(); + foreach ($parents as $parent) { + $childIds = $parent->descendants->pluck('id')->toArray(); + array_push($childIds, $parent->id); + $ids = $childIds + $ids; + } + $locationIds = $ids; } $query->whereIn($this->getTable() . '.location_id', $locationIds)->distinct(); @@ -621,20 +610,14 @@ protected function filterOrganisations(Builder $query, null|string|array $value } if ($this->filterOption('children')) { - $orgs = DB::select(" - WITH RECURSIVE organisation_tree AS ( - -- Select parents - SELECT * FROM organisations WHERE id IN (" . implode(',', $orgIds) . ") - UNION ALL - -- Recursively get all descendants - SELECT r.* FROM organisations r - INNER JOIN organisation_tree rt ON r.organisation_id = rt.id - ) - SELECT * FROM organisation_tree - "); - $orgIds = collect($orgs)->pluck('id'); // Extract IDs - $query->whereIn($this->getTable() . '.organisation_id', $orgIds)->distinct(); - return; + $ids = []; + $parents = Organisation::whereIn('id', $orgIds)->with('descendants')->get(); + foreach ($parents as $parent) { + $childIds = $parent->descendants->pluck('id')->toArray(); + array_push($childIds, $parent->id); + $ids = $childIds + $ids; + } + $value = $ids; } foreach ($value as $v) { @@ -664,16 +647,28 @@ protected function filterFamilies(Builder $query, null|string|array $value = nul $value = [$value]; } + $familyIds = []; + foreach ($value as $v) { + $familyIds[] = (int) $v; + } + if ($this->filterOption('exclude')) { - $familyIds = []; - foreach ($value as $v) { - $familyIds[] = (int) $v; - } $query->whereRaw('(select count(*) from character_family as cr where cr.character_id = ' . $this->getTable() . '.id and cr.family_id in (' . implode(', ', $familyIds) . ')) = 0'); return; } + if ($this->filterOption('children')) { + $ids = []; + $parents = Family::whereIn('id', $familyIds)->with('descendants')->get(); + foreach ($parents as $parent) { + $childIds = $parent->descendants->pluck('id')->toArray(); + array_push($childIds, $parent->id); + $ids = $childIds + $ids; + } + $value = $ids; + } + foreach ($value as $v) { $v = (int) $v; $query diff --git a/app/Services/FilterService.php b/app/Services/FilterService.php index 47708dabad..06797c24e3 100644 --- a/app/Services/FilterService.php +++ b/app/Services/FilterService.php @@ -128,6 +128,10 @@ protected function prepareFilters(array $availableFilters = []): self $this->filters = []; } + foreach (['tags', 'locations', 'organisations', 'races', 'families'] as $key) { + $this->checkFilterData($key, $availableFilters); + } + // If we have data but no "tags" array, it's empty if (!empty($this->data) && in_array('tags', $availableFilters) && !isset($this->data['tags'])) { // Not calling from a page or order result, we can junk the filters @@ -246,6 +250,17 @@ protected function prepareOrder(array $availableFields = []): self return $this; } + private function checkFilterData(string $key, array $availableFilters): void + { + // If we have data but no array, it's empty + if (!empty($this->data) && in_array($key, $availableFilters) && !isset($this->data[$key])) { + // Not calling from a page or order result, we can junk the filters + if (empty($this->data['page']) && empty($this->data['order'])) { + unset($this->data[$key]); + } + } + } + /** * @return $this */ diff --git a/resources/views/filters/form.blade.php b/resources/views/filters/form.blade.php index 113fb5632a..bf72ff2b8d 100644 --- a/resources/views/filters/form.blade.php +++ b/resources/views/filters/form.blade.php @@ -24,20 +24,18 @@ @if (is_array($field)) single($field['field']); if (!empty($value) && $field['type'] == 'select2') { $modelclass = new $field['model']; $model = $modelclass->find($value); } // Support for fields with multiple models selected - if (Illuminate\Support\Arr::get($field, 'multiple') === true && $field['type'] == 'selectMultiple') { + if (Arr::get($field, 'multiple') === true && $field['type'] == 'selectMultiple') { $value = $filterService->filterValue($field['field']); if (!empty($value)) { $modelclass = new $field['model']; $models = $modelclass->whereIn('id', $value)->get()->pluck('name', 'id')->toArray(); - } else { - unset($models); } } ?> From dac57988c4b439c4a49d5c28a61c76d6ade94814 Mon Sep 17 00:00:00 2001 From: spitfire305 Date: Thu, 6 Feb 2025 15:39:17 +0000 Subject: [PATCH 4/6] Fix styling --- app/Models/Concerns/HasFilters.php | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/app/Models/Concerns/HasFilters.php b/app/Models/Concerns/HasFilters.php index cc557df71d..0c1de03d36 100644 --- a/app/Models/Concerns/HasFilters.php +++ b/app/Models/Concerns/HasFilters.php @@ -10,7 +10,6 @@ use App\Models\Family; use App\Models\Organisation; use App\Models\Race; -use Illuminate\Support\Facades\DB; /** * HasFilters @@ -189,7 +188,7 @@ protected function extractSearchOperator($value, string $key): void { $operator = 'like'; $filterValue = $value; - if (!in_array($key, ['tags', 'locations', 'organisations', 'races', 'families'])) { + if (!in_array($key, ['tags', 'locations', 'organisations', 'races', 'families'])) { if ($value == '!!') { $operator = 'IS NULL'; $filterValue = null; @@ -668,7 +667,7 @@ protected function filterFamilies(Builder $query, null|string|array $value = nul } $value = $ids; } - + foreach ($value as $v) { $v = (int) $v; $query From fdc74ef4056a6ca4df6b41811c6d7d74a1085b98 Mon Sep 17 00:00:00 2001 From: Spitfire Date: Thu, 6 Feb 2025 11:14:02 -0600 Subject: [PATCH 5/6] Fixed queries --- app/Models/Concerns/HasFilters.php | 33 +++++++++++------------------- 1 file changed, 12 insertions(+), 21 deletions(-) diff --git a/app/Models/Concerns/HasFilters.php b/app/Models/Concerns/HasFilters.php index 0c1de03d36..7f6488a9e6 100644 --- a/app/Models/Concerns/HasFilters.php +++ b/app/Models/Concerns/HasFilters.php @@ -506,13 +506,10 @@ protected function filterRaces(Builder $query, null|string|array $value = null): $value = $ids; } - foreach ($value as $v) { - $v = (int) $v; - $query - ->leftJoin('character_race as cr' . $v, "cr{$v}.character_id", $this->getTable() . '.id') - ->where("cr{$v}.race_id", $v) - ; - } + $values = collect($value)->map(fn($v) => (int) $v)->toArray(); // Ensure values are integers + $query + ->leftJoin('character_race as cr', 'cr.character_id', '=', $this->getTable() . '.id') + ->whereIn('cr.race_id', $values); } /** @@ -619,13 +616,10 @@ protected function filterOrganisations(Builder $query, null|string|array $value $value = $ids; } - foreach ($value as $v) { - $v = (int) $v; - $query - ->leftJoin('organisation_member as cr' . $v, "cr{$v}.character_id", $this->getTable() . '.id') - ->where("cr{$v}.organisation_id", $v) - ; - } + $values = collect($value)->map(fn($v) => (int) $v)->toArray(); // Ensure values are integers + $query + ->leftJoin('organisation_member as om', 'om.character_id', '=', $this->getTable() . '.id') + ->whereIn('om.organisation_id', $values); } /** @@ -668,13 +662,10 @@ protected function filterFamilies(Builder $query, null|string|array $value = nul $value = $ids; } - foreach ($value as $v) { - $v = (int) $v; - $query - ->leftJoin('character_family as cr' . $v, "cr{$v}.character_id", $this->getTable() . '.id') - ->where("cr{$v}.family_id", $v) - ; - } + $values = collect($value)->map(fn($v) => (int) $v)->toArray(); // Ensure values are integers + $query + ->leftJoin('character_family as cf', 'cf.character_id', '=', $this->getTable() . '.id') + ->whereIn('cf.family_id', $values); } /** From bd4679ac32d569eeaa2511f312b16dbea08bad5c Mon Sep 17 00:00:00 2001 From: spitfire305 Date: Thu, 6 Feb 2025 17:15:17 +0000 Subject: [PATCH 6/6] Fix styling --- app/Models/Concerns/HasFilters.php | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/app/Models/Concerns/HasFilters.php b/app/Models/Concerns/HasFilters.php index 7f6488a9e6..c3be45063d 100644 --- a/app/Models/Concerns/HasFilters.php +++ b/app/Models/Concerns/HasFilters.php @@ -506,7 +506,7 @@ protected function filterRaces(Builder $query, null|string|array $value = null): $value = $ids; } - $values = collect($value)->map(fn($v) => (int) $v)->toArray(); // Ensure values are integers + $values = collect($value)->map(fn ($v) => (int) $v)->toArray(); // Ensure values are integers $query ->leftJoin('character_race as cr', 'cr.character_id', '=', $this->getTable() . '.id') ->whereIn('cr.race_id', $values); @@ -616,7 +616,7 @@ protected function filterOrganisations(Builder $query, null|string|array $value $value = $ids; } - $values = collect($value)->map(fn($v) => (int) $v)->toArray(); // Ensure values are integers + $values = collect($value)->map(fn ($v) => (int) $v)->toArray(); // Ensure values are integers $query ->leftJoin('organisation_member as om', 'om.character_id', '=', $this->getTable() . '.id') ->whereIn('om.organisation_id', $values); @@ -662,7 +662,7 @@ protected function filterFamilies(Builder $query, null|string|array $value = nul $value = $ids; } - $values = collect($value)->map(fn($v) => (int) $v)->toArray(); // Ensure values are integers + $values = collect($value)->map(fn ($v) => (int) $v)->toArray(); // Ensure values are integers $query ->leftJoin('character_family as cf', 'cf.character_id', '=', $this->getTable() . '.id') ->whereIn('cf.family_id', $values);