Add searching and filtering for events

This commit is contained in:
philipp lang 2024-01-29 22:07:33 +01:00
parent e45a59c5ff
commit 28c821eeaf
8 changed files with 119 additions and 29 deletions

View File

@ -2,11 +2,13 @@
namespace App\Form\Actions; namespace App\Form\Actions;
use App\Form\FilterScope;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Form\Resources\FormResource; use App\Form\Resources\FormResource;
use Illuminate\Pagination\LengthAwarePaginator; use Illuminate\Pagination\LengthAwarePaginator;
use Inertia\Inertia; use Inertia\Inertia;
use Inertia\Response; use Inertia\Response;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction; use Lorisleiva\Actions\Concerns\AsAction;
class FormIndexAction class FormIndexAction
@ -16,18 +18,18 @@ class FormIndexAction
/** /**
* @return LengthAwarePaginator<Form> * @return LengthAwarePaginator<Form>
*/ */
public function handle(): LengthAwarePaginator public function handle(string $filter): LengthAwarePaginator
{ {
return Form::paginate(15); return FilterScope::fromRequest($filter)->getQuery()->paginate(15);
} }
public function asController(): Response public function asController(ActionRequest $request): Response
{ {
session()->put('menu', 'form'); session()->put('menu', 'form');
session()->put('title', 'Veranstaltungen'); session()->put('title', 'Veranstaltungen');
return Inertia::render('form/Index', [ return Inertia::render('form/Index', [
'data' => FormResource::collection($this->handle()), 'data' => FormResource::collection($this->handle($request->input('filter', ''))),
]); ]);
} }
} }

View File

@ -2,9 +2,9 @@
namespace App\Form; namespace App\Form;
use Laravel\Scout\Builder;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Lib\Filter; use App\Lib\Filter;
use Illuminate\Database\Eloquent\Builder;
use Spatie\LaravelData\Attributes\MapInputName; use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName; use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper; use Spatie\LaravelData\Mappers\SnakeCaseMapper;
@ -16,25 +16,28 @@ use Spatie\LaravelData\Mappers\SnakeCaseMapper;
#[MapOutputName(SnakeCaseMapper::class)] #[MapOutputName(SnakeCaseMapper::class)]
class FilterScope extends Filter class FilterScope extends Filter
{ {
public function __construct() public function __construct(
{ public ?string $search = '',
public bool $past = false,
) {
} }
/** public function getQuery(): Builder
* {@inheritdoc}
*/
public function locks(): array
{ {
return []; $this->search = $this->search ?: '';
}
/** return Form::search($this->search, function ($engine, string $query, array $options) {
* @param Builder<Form> $query $options['sort'] = ['from:asc'];
*
* @return Builder<Form> $filters = collect([]);
*/
public function apply(Builder $query): Builder if ($this->past === false) {
{ $filters->push('to > ' . now()->timestamp);
return $query; }
$options['filter'] = $filters->implode(' AND ');
return $engine->search($query, $options);
});
} }
} }

View File

@ -2,11 +2,10 @@
namespace App\Form\Models; namespace App\Form\Models;
use App\Form\FilterScope;
use Cviebrock\EloquentSluggable\Sluggable; use Cviebrock\EloquentSluggable\Sluggable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory; use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model; use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
use Spatie\Image\Manipulations; use Spatie\Image\Manipulations;
use Spatie\MediaLibrary\HasMedia; use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia; use Spatie\MediaLibrary\InteractsWithMedia;
@ -19,6 +18,7 @@ class Form extends Model implements HasMedia
use Sluggable; use Sluggable;
use InteractsWithMedia; use InteractsWithMedia;
use DefersUploads; use DefersUploads;
use Searchable;
public $guarded = []; public $guarded = [];
@ -50,13 +50,20 @@ class Form extends Model implements HasMedia
/** @var array<int, string> */ /** @var array<int, string> */
public $dates = ['from', 'to', 'registration_from', 'registration_until']; public $dates = ['from', 'to', 'registration_from', 'registration_until'];
// --------------------------------- Searching ---------------------------------
// *****************************************************************************
/** /**
* @param Builder<self> $query * Get the indexable data array for the model.
* *
* @return Builder<self> * @return array<string, mixed>
*/ */
public function scopeWithFilter(Builder $query, FilterScope $filter): Builder public function toSearchableArray()
{ {
return $filter->apply($query); return [
'from' => $this->from->timestamp,
'to' => $this->to->timestamp,
'name' => $this->name,
];
} }
} }

View File

@ -3,6 +3,7 @@
namespace App\Form\Resources; namespace App\Form\Resources;
use App\Form\Fields\Field; use App\Form\Fields\Field;
use App\Form\FilterScope;
use App\Form\Models\Form; use App\Form\Models\Form;
use App\Form\Models\Formtemplate; use App\Form\Models\Formtemplate;
use App\Group; use App\Group;
@ -55,6 +56,7 @@ class FormResource extends JsonResource
'base_url' => url(''), 'base_url' => url(''),
'groups' => Group::forSelect(), 'groups' => Group::forSelect(),
'fields' => Field::asMeta(), 'fields' => Field::asMeta(),
'filter' => FilterScope::fromRequest(request()->input('filter', '')),
'links' => [ 'links' => [
'store' => route('form.store'), 'store' => route('form.store'),
'formtemplate_index' => route('formtemplate.index'), 'formtemplate_index' => route('formtemplate.index'),

View File

@ -1,5 +1,6 @@
<?php <?php
use App\Form\Models\Form;
use App\Member\Member; use App\Member\Member;
return [ return [
@ -140,6 +141,12 @@ return [
'searchableAttributes' => ['fullname', 'address'], 'searchableAttributes' => ['fullname', 'address'],
'sortableAttributes' => ['lastname', 'firstname'], 'sortableAttributes' => ['lastname', 'firstname'],
'displayedAttributes' => ['age_group_icon', 'group_name', 'links', 'is_leader', 'lastname', 'firstname', 'fullname', 'address', 'ausstand', 'birthday', 'id', 'memberships', 'bill_kind', 'group_id'], 'displayedAttributes' => ['age_group_icon', 'group_name', 'links', 'is_leader', 'lastname', 'firstname', 'fullname', 'address', 'ausstand', 'birthday', 'id', 'memberships', 'bill_kind', 'group_id'],
],
Form::class => [
'filterableAttributes' => ['to'],
'searchableAttributes' => ['name'],
'sortableAttributes' => ['from',],
'displayedAttributes' => ['from', 'name', 'id', 'to'],
] ]
], ],
], ],

View File

@ -10,13 +10,14 @@ export function useIndex(props, siteName) {
const inner = { const inner = {
data: ref(rawProps.data), data: ref(rawProps.data),
meta: ref(rawProps.meta), meta: ref(rawProps.meta),
filter: ref(rawProps.meta.filter ? rawProps.meta.filter : {}),
}; };
function toFilterString(data) { function toFilterString(data) {
return btoa(encodeURIComponent(JSON.stringify(data))); return btoa(encodeURIComponent(JSON.stringify(data)));
} }
const filterString = computed(() => toFilterString(inner.meta.value.filter)); const filterString = computed(() => toFilterString(inner.filter.value));
function reload(resetPage = true, withMeta = true, data) { function reload(resetPage = true, withMeta = true, data) {
data = { data = {
@ -76,6 +77,15 @@ export function useIndex(props, siteName) {
single.value = null; single.value = null;
} }
function getFilter(value) {
return inner.filter.value[value];
}
function setFilter(key, value) {
inner.filter.value[key] = value;
reload(true);
}
startListener(); startListener();
onBeforeUnmount(() => stopListener()); onBeforeUnmount(() => stopListener());
@ -93,6 +103,8 @@ export function useIndex(props, siteName) {
remove, remove,
cancel, cancel,
axios, axios,
setFilter,
getFilter,
}; };
} }

View File

@ -70,6 +70,13 @@
</template> </template>
</ui-popup> </ui-popup>
<page-filter breakpoint="xl">
<f-text id="search" :model-value="getFilter('search')" name="search" label="Suchen …" size="sm"
@update:model-value="setFilter('search', $event)"></f-text>
<f-switch id="past" :model-value="getFilter('past')" label="vergangene zeigen" size="sm"
@update:model-value="setFilter('past', $event)"></f-switch>
</page-filter>
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm"> <table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm">
<thead> <thead>
<th>Name</th> <th>Name</th>
@ -108,7 +115,7 @@ import { indexProps, useIndex } from '../../composables/useInertiaApiIndex.js';
import FormBuilder from '../formtemplate/FormBuilder.vue'; import FormBuilder from '../formtemplate/FormBuilder.vue';
const props = defineProps(indexProps); const props = defineProps(indexProps);
var { meta, data, reloadPage, create, single, edit, cancel, submit, remove } = useIndex(props.data, 'form'); var { meta, data, reloadPage, create, single, edit, cancel, submit, remove, getFilter, setFilter } = useIndex(props.data, 'form');
const active = ref(0); const active = ref(0);
const deleting = ref(null); const deleting = ref(null);

View File

@ -0,0 +1,50 @@
<?php
namespace Tests\EndToEnd;
use App\Form\Models\Form;
use Tests\EndToEndTestCase;
class FormIndexTest extends EndToEndTestCase
{
public function testItHandlesFullTextSearch()
{
$this->withoutExceptionHandling()->login()->loginNami();
Form::factory()->to(now()->addYear())->name('ZEM 2024')->create();
Form::factory()->to(now()->addYear())->name('Rover-Spek 2025')->create();
sleep(1);
$this->callFilter('form.index', ['search' => 'ZEM'])
->assertInertiaCount('data.data', 1);
$this->callFilter('form.index', [])
->assertInertiaCount('data.data', 2);
}
public function testItOrdersByStartDateDesc()
{
$this->withoutExceptionHandling()->login()->loginNami();
$form1 = Form::factory()->from(now()->addDays(4))->to(now()->addYear())->create();
$form2 = Form::factory()->from(now()->addDays(3))->to(now()->addYear())->create();
$form3 = Form::factory()->from(now()->addDays(2))->to(now()->addYear())->create();
sleep(1);
$this->callFilter('form.index', [])
->assertInertiaPath('data.data.0.id', $form3->id)
->assertInertiaPath('data.data.1.id', $form2->id)
->assertInertiaPath('data.data.2.id', $form1->id);
}
public function testItShowsPastEvents()
{
$this->withoutExceptionHandling()->login()->loginNami();
Form::factory()->count(5)->to(now()->subDays(2))->create();
Form::factory()->count(3)->to(now()->subDays(5))->create();
Form::factory()->count(2)->to(now()->addDays(3))->create();
sleep(1);
$this->callFilter('form.index', ['past' => true])
->assertInertiaCount('data.data', 10);
$this->callFilter('form.index', [])
->assertInertiaCount('data.data', 2);
}
}