Add searching and filtering for events
This commit is contained in:
parent
e45a59c5ff
commit
28c821eeaf
|
@ -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', ''))),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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'),
|
||||
|
|
|
@ -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'],
|
||||
]
|
||||
],
|
||||
],
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
Loading…
Reference in New Issue