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;
use App\Form\FilterScope;
use App\Form\Models\Form;
use App\Form\Resources\FormResource;
use Illuminate\Pagination\LengthAwarePaginator;
use Inertia\Inertia;
use Inertia\Response;
use Lorisleiva\Actions\ActionRequest;
use Lorisleiva\Actions\Concerns\AsAction;
class FormIndexAction
@ -16,18 +18,18 @@ class FormIndexAction
/**
* @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('title', 'Veranstaltungen');
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;
use Laravel\Scout\Builder;
use App\Form\Models\Form;
use App\Lib\Filter;
use Illuminate\Database\Eloquent\Builder;
use Spatie\LaravelData\Attributes\MapInputName;
use Spatie\LaravelData\Attributes\MapOutputName;
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
@ -16,25 +16,28 @@ use Spatie\LaravelData\Mappers\SnakeCaseMapper;
#[MapOutputName(SnakeCaseMapper::class)]
class FilterScope extends Filter
{
public function __construct()
{
public function __construct(
public ?string $search = '',
public bool $past = false,
) {
}
/**
* {@inheritdoc}
*/
public function locks(): array
public function getQuery(): Builder
{
return [];
}
$this->search = $this->search ?: '';
/**
* @param Builder<Form> $query
*
* @return Builder<Form>
*/
public function apply(Builder $query): Builder
{
return $query;
return Form::search($this->search, function ($engine, string $query, array $options) {
$options['sort'] = ['from:asc'];
$filters = collect([]);
if ($this->past === false) {
$filters->push('to > ' . now()->timestamp);
}
$options['filter'] = $filters->implode(' AND ');
return $engine->search($query, $options);
});
}
}

View File

@ -2,11 +2,10 @@
namespace App\Form\Models;
use App\Form\FilterScope;
use Cviebrock\EloquentSluggable\Sluggable;
use Illuminate\Database\Eloquent\Builder;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Laravel\Scout\Searchable;
use Spatie\Image\Manipulations;
use Spatie\MediaLibrary\HasMedia;
use Spatie\MediaLibrary\InteractsWithMedia;
@ -19,6 +18,7 @@ class Form extends Model implements HasMedia
use Sluggable;
use InteractsWithMedia;
use DefersUploads;
use Searchable;
public $guarded = [];
@ -50,13 +50,20 @@ class Form extends Model implements HasMedia
/** @var array<int, string> */
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;
use App\Form\Fields\Field;
use App\Form\FilterScope;
use App\Form\Models\Form;
use App\Form\Models\Formtemplate;
use App\Group;
@ -55,6 +56,7 @@ class FormResource extends JsonResource
'base_url' => url(''),
'groups' => Group::forSelect(),
'fields' => Field::asMeta(),
'filter' => FilterScope::fromRequest(request()->input('filter', '')),
'links' => [
'store' => route('form.store'),
'formtemplate_index' => route('formtemplate.index'),

View File

@ -1,5 +1,6 @@
<?php
use App\Form\Models\Form;
use App\Member\Member;
return [
@ -140,6 +141,12 @@ return [
'searchableAttributes' => ['fullname', 'address'],
'sortableAttributes' => ['lastname', 'firstname'],
'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 = {
data: ref(rawProps.data),
meta: ref(rawProps.meta),
filter: ref(rawProps.meta.filter ? rawProps.meta.filter : {}),
};
function toFilterString(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) {
data = {
@ -76,6 +77,15 @@ export function useIndex(props, siteName) {
single.value = null;
}
function getFilter(value) {
return inner.filter.value[value];
}
function setFilter(key, value) {
inner.filter.value[key] = value;
reload(true);
}
startListener();
onBeforeUnmount(() => stopListener());
@ -93,6 +103,8 @@ export function useIndex(props, siteName) {
remove,
cancel,
axios,
setFilter,
getFilter,
};
}

View File

@ -70,6 +70,13 @@
</template>
</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">
<thead>
<th>Name</th>
@ -108,7 +115,7 @@ import { indexProps, useIndex } from '../../composables/useInertiaApiIndex.js';
import FormBuilder from '../formtemplate/FormBuilder.vue';
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 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);
}
}