diff --git a/app/Form/Actions/ParticipantIndexAction.php b/app/Form/Actions/ParticipantIndexAction.php index 4f8bdd2c..bb7e3a5f 100644 --- a/app/Form/Actions/ParticipantIndexAction.php +++ b/app/Form/Actions/ParticipantIndexAction.php @@ -8,7 +8,6 @@ use App\Form\Resources\ParticipantResource; use App\Form\Scopes\ParticipantFilterScope; use Illuminate\Database\Eloquent\Relations\HasMany; use Illuminate\Http\Resources\Json\AnonymousResourceCollection; -use Illuminate\Pagination\LengthAwarePaginator; use Laravel\Scout\Builder; use Lorisleiva\Actions\Concerns\AsAction; @@ -25,22 +24,14 @@ class ParticipantIndexAction ->query(fn ($q) => $q->withCount('children')->with('form')); } - /** - * @return LengthAwarePaginator - */ - public function handle(Form $form, ParticipantFilterScope $filter): LengthAwarePaginator - { - return $this->getQuery($form, $filter)->paginate(15); - } - public function asController(Form $form, ?int $parent = null): AnonymousResourceCollection { - $filter = ParticipantFilterScope::fromRequest(request()->input('filter')); + $filter = ParticipantFilterScope::fromRequest(request()->input('filter', ''))->parent($parent); $data = match ($parent) { - null => $this->handle($form, $filter), - -1 => $this->getQuery($form, $filter)->where('parent_id', null)->paginate(15), - default => $this->getQuery($form, $filter)->where('parent_id', $parent)->get(), + null => $this->getQuery($form, $filter)->paginate(15), // initial all elements - paginate + -1 => $this->getQuery($form, $filter)->paginate(15), // initial root elements - parinate + default => $this->getQuery($form, $filter)->get(), // specific parent element - show all }; return ParticipantResource::collection($data)->additional(['meta' => ParticipantResource::meta($form)]); diff --git a/app/Form/Actions/UpdateParticipantSearchIndexAction.php b/app/Form/Actions/UpdateParticipantSearchIndexAction.php index 5c75640d..dc45407b 100644 --- a/app/Form/Actions/UpdateParticipantSearchIndexAction.php +++ b/app/Form/Actions/UpdateParticipantSearchIndexAction.php @@ -14,7 +14,7 @@ class UpdateParticipantSearchIndexAction $form->searchableUsing()->updateIndexSettings( $form->participantsSearchableAs(), [ - 'filterableAttributes' => $form->getFields()->filterables()->getKeys(), + 'filterableAttributes' => [...$form->getFields()->filterables()->getKeys(), 'parent-id'], 'searchableAttributes' => $form->getFields()->searchables()->getKeys(), 'sortableAttributes' => [], 'displayedAttributes' => [...$form->getFields()->filterables()->getKeys(), ...$form->getFields()->searchables()->getKeys(), 'id'], diff --git a/app/Form/Contracts/Filterable.php b/app/Form/Contracts/Filterable.php new file mode 100644 index 00000000..ddc36fc9 --- /dev/null +++ b/app/Form/Contracts/Filterable.php @@ -0,0 +1,9 @@ +filter(fn ($field) => $field instanceof Filterable); } /** diff --git a/app/Form/Fields/CheckboxField.php b/app/Form/Fields/CheckboxField.php index c649b10b..669aaa72 100644 --- a/app/Form/Fields/CheckboxField.php +++ b/app/Form/Fields/CheckboxField.php @@ -2,6 +2,7 @@ namespace App\Form\Fields; +use App\Form\Contracts\Filterable; use App\Form\Matchers\BooleanMatcher; use App\Form\Matchers\Matcher; use App\Form\Models\Form; @@ -9,9 +10,8 @@ use App\Form\Models\Participant; use App\Form\Presenters\BooleanPresenter; use App\Form\Presenters\Presenter; use Faker\Generator; -use Illuminate\Validation\Rule; -class CheckboxField extends Field +class CheckboxField extends Field implements Filterable { public bool $required; public string $description; @@ -86,4 +86,11 @@ class CheckboxField extends Field { return app(BooleanMatcher::class); } + + public function filter($value): string + { + $asString = $value ? 'true' : 'false'; + + return "{$this->key} = $asString"; + } } diff --git a/app/Form/Fields/DropdownField.php b/app/Form/Fields/DropdownField.php index e903ac4b..6bc27367 100644 --- a/app/Form/Fields/DropdownField.php +++ b/app/Form/Fields/DropdownField.php @@ -2,6 +2,7 @@ namespace App\Form\Fields; +use App\Form\Contracts\Filterable; use App\Form\Matchers\Matcher; use App\Form\Matchers\SingleValueMatcher; use App\Form\Models\Form; @@ -9,7 +10,7 @@ use App\Form\Models\Participant; use Faker\Generator; use Illuminate\Validation\Rule; -class DropdownField extends Field +class DropdownField extends Field implements Filterable { public bool $required; /** @var array */ @@ -87,4 +88,14 @@ class DropdownField extends Field { return app(SingleValueMatcher::class); } + + /** @inheritdoc */ + public function filter($value): string + { + if (is_null($value)) { + return "{$this->key} IS NULL"; + } + + return $this->key . ' = \'' . $value . '\''; + } } diff --git a/app/Form/Fields/RadioField.php b/app/Form/Fields/RadioField.php index 07705278..c3618c6a 100644 --- a/app/Form/Fields/RadioField.php +++ b/app/Form/Fields/RadioField.php @@ -2,6 +2,7 @@ namespace App\Form\Fields; +use App\Form\Contracts\Filterable; use App\Form\Matchers\Matcher; use App\Form\Matchers\SingleValueMatcher; use App\Form\Models\Form; @@ -9,7 +10,7 @@ use App\Form\Models\Participant; use Faker\Generator; use Illuminate\Validation\Rule; -class RadioField extends Field +class RadioField extends Field implements Filterable { public bool $required; /** @var array */ @@ -87,4 +88,13 @@ class RadioField extends Field { return app(SingleValueMatcher::class); } + + public function filter($value): string + { + if (is_null($value)) { + return "{$this->key} IS NULL"; + } + + return $this->key . ' = \'' . $value . '\''; + } } diff --git a/app/Form/Models/Participant.php b/app/Form/Models/Participant.php index 3c8577dc..f28247e6 100644 --- a/app/Form/Models/Participant.php +++ b/app/Form/Models/Participant.php @@ -111,6 +111,6 @@ class Participant extends Model implements Preventable public function toSearchableArray(): array { - return $this->data; + return [...$this->data, 'parent-id' => $this->parent_id]; } } diff --git a/app/Form/Scopes/ParticipantFilterScope.php b/app/Form/Scopes/ParticipantFilterScope.php index 339c998c..7f3606ed 100644 --- a/app/Form/Scopes/ParticipantFilterScope.php +++ b/app/Form/Scopes/ParticipantFilterScope.php @@ -21,7 +21,7 @@ class ParticipantFilterScope extends ScoutFilter { public static string $nan = 'deeb3ef4-d185-44b1-a4bc-0a4e7addebc3d8900c6f-a344-4afb-b54e-065ed483a7ba'; - public Form $form; + private Form $form; /** * @param array $data @@ -30,6 +30,7 @@ class ParticipantFilterScope extends ScoutFilter public array $data = [], public string $search = '', public array $options = [], + public ?int $parent = null ) { } @@ -37,7 +38,28 @@ class ParticipantFilterScope extends ScoutFilter { $this->search = $this->search ?: ''; - return Participant::search($this->search)->within($this->form->participantsSearchableAs()); + return Participant::search($this->search, function ($engine, string $query, array $options) { + $filter = collect([]); + + foreach ($this->form->getFields()->filterables() as $field) { + if ($this->data[$field->key] === static::$nan) { + continue; + } + $filter->push($field->filter($this->data[$field->key])); + } + + if ($this->parent === -1) { + $filter->push('parent-id IS NULL'); + } + + if ($this->parent !== null && $this->parent !== -1) { + $filter->push('parent-id = ' . $this->parent); + } + + $options['filter'] = $filter->map(fn ($expression) => "($expression)")->implode(' AND '); + + return $engine->search($query, [...$this->options, ...$options]); + })->within($this->form->participantsSearchableAs()); } public function setForm(Form $form): self @@ -53,18 +75,10 @@ class ParticipantFilterScope extends ScoutFilter return $this; } - /** - * @inheritdoc - */ - public function apply(Builder $query): Builder + public function parent(?int $parent): self { - foreach ($this->data as $key => $value) { - if ($value === static::$nan) { - continue; - } - $query = $query->where('data->' . $key, $value); - } + $this->parent = $parent; - return $query; + return $this; } } diff --git a/database/migrations/2024_12_11_235710_update_form_searching.php b/database/migrations/2024_12_11_235710_update_form_searching.php new file mode 100644 index 00000000..f1206dbd --- /dev/null +++ b/database/migrations/2024_12_11_235710_update_form_searching.php @@ -0,0 +1,29 @@ +participants as $participant) { + $participant->searchable(); + } + } + } + + /** + * Reverse the migrations. + */ + public function down(): void + { + // + } +}; diff --git a/resources/js/composables/useApiIndex.js b/resources/js/composables/useApiIndex.js index 75b5ca50..950ad737 100644 --- a/resources/js/composables/useApiIndex.js +++ b/resources/js/composables/useApiIndex.js @@ -28,7 +28,7 @@ export function useApiIndex(firstUrl, siteName = null) { inner.meta.value = response.meta; } - async function reloadPage(page) { + async function reloadPage(page, p = {}) { inner.meta.value.current_page = page; await reload(false); } diff --git a/resources/js/views/form/Participants.vue b/resources/js/views/form/Participants.vue index 62be9130..a46c8666 100644 --- a/resources/js/views/form/Participants.vue +++ b/resources/js/views/form/Participants.vue @@ -23,6 +23,7 @@
- +
diff --git a/tests/EndToEnd/Form/ParticipantIndexActionTest.php b/tests/EndToEnd/Form/ParticipantIndexActionTest.php index 8e29bb82..02fc2c1a 100644 --- a/tests/EndToEnd/Form/ParticipantIndexActionTest.php +++ b/tests/EndToEnd/Form/ParticipantIndexActionTest.php @@ -91,6 +91,9 @@ it('testItFiltersParticipantsByCheckboxValue', function () { ->has(Participant::factory()->data(['check' => false])->count(2)) ->create(); + sleep(2); + $this->callFilter('form.participant.index', ['data' => []], ['form' => $form]) + ->assertJsonCount(3, 'data'); $this->callFilter('form.participant.index', ['data' => ['check' => ParticipantFilterScope::$nan]], ['form' => $form]) ->assertJsonCount(3, 'data'); $this->callFilter('form.participant.index', ['data' => ['check' => true]], ['form' => $form]) @@ -139,6 +142,8 @@ it('testItFiltersParticipantsByDropdownValue', function () { ->assertJsonCount(2, 'data'); $this->callFilter('form.participant.index', ['data' => ['drop' => 'B']], ['form' => $form]) ->assertJsonCount(4, 'data'); + $this->callFilter('form.participant.index', ['data' => ['drop' => 'Z*Z']], ['form' => $form]) + ->assertJsonCount(0, 'data'); }); it('testItFiltersParticipantsByRadioValue', function () {