Add filter for participants
continuous-integration/drone/push Build is failing
Details
continuous-integration/drone/push Build is failing
Details
This commit is contained in:
parent
9954ba1ee4
commit
8f285b4aa6
|
@ -4,6 +4,7 @@ namespace App\Form\Resources;
|
|||
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use App\Form\Scopes\ParticipantFilterScope;
|
||||
use Illuminate\Http\Resources\Json\JsonResource;
|
||||
use Illuminate\Support\Collection;
|
||||
|
||||
|
@ -43,7 +44,17 @@ class ParticipantResource extends JsonResource
|
|||
'id' => $field->key,
|
||||
'display_attribute' => $field->getDisplayAttribute(),
|
||||
]);
|
||||
|
||||
$filterData = $form->getFields()
|
||||
->map(fn ($field) => [
|
||||
...$field->toArray(),
|
||||
'base_type' => class_basename($field),
|
||||
]);
|
||||
|
||||
return [
|
||||
'filter' => ParticipantFilterScope::fromRequest(request()->input('filter', ''))->setForm($form),
|
||||
'default_filter_value' => ParticipantFilterScope::$nan,
|
||||
'filters' => $filterData,
|
||||
'form_meta' => $form->meta,
|
||||
'links' => [
|
||||
'update_form_meta' => route('form.update-meta', ['form' => $form]),
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
namespace App\Form\Scopes;
|
||||
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use App\Lib\Filter;
|
||||
use Illuminate\Database\Eloquent\Builder;
|
||||
use Illuminate\Support\Arr;
|
||||
use Spatie\LaravelData\Attributes\MapInputName;
|
||||
use Spatie\LaravelData\Attributes\MapOutputName;
|
||||
use Spatie\LaravelData\Mappers\SnakeCaseMapper;
|
||||
|
@ -18,9 +20,23 @@ class ParticipantFilterScope extends Filter
|
|||
{
|
||||
public function __construct(
|
||||
public ?int $parent = null,
|
||||
public array $data = [],
|
||||
) {
|
||||
}
|
||||
|
||||
public static string $nan = 'deeb3ef4-d185-44b1-a4bc-0a4e7addebc3d8900c6f-a344-4afb-b54e-065ed483a7ba';
|
||||
|
||||
public function setForm(Form $form): self
|
||||
{
|
||||
foreach ($form->getFields() as $field) {
|
||||
if (!Arr::has($this->data, $field->key)) {
|
||||
data_set($this->data, $field->key, static::$nan);
|
||||
}
|
||||
}
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @inheritdoc
|
||||
*/
|
||||
|
@ -34,6 +50,13 @@ class ParticipantFilterScope extends Filter
|
|||
$query = $query->where('parent_id', $this->parent);
|
||||
}
|
||||
|
||||
foreach ($this->data as $key => $value) {
|
||||
if ($value === static::$nan) {
|
||||
continue;
|
||||
}
|
||||
$query = $query->where('data->' . $key, $value);
|
||||
}
|
||||
|
||||
return $query;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,8 +5,8 @@
|
|||
<span v-show="required" class="text-red-800"> *</span>
|
||||
</span>
|
||||
<div class="real-field-wrap" :class="`size-${size}`">
|
||||
<select :disabled="disabled" :name="name" :value="modelValue" @change="trigger">
|
||||
<option v-if="placeholder" value="">{{ placeholder }}</option>
|
||||
<select v-model="inner" :disabled="disabled" :name="name">
|
||||
<option v-if="placeholder" :value="def">{{ placeholder }}</option>
|
||||
|
||||
<option v-for="option in parsedOptions" :key="option.id" :value="option.id">{{ option.name }}</option>
|
||||
</select>
|
||||
|
@ -25,84 +25,83 @@
|
|||
</label>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script setup>
|
||||
import {computed, ref} from 'vue';
|
||||
import map from 'lodash/map';
|
||||
|
||||
export default {
|
||||
props: {
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: function () {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
id: {},
|
||||
inset: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
default: function () {
|
||||
return 'base';
|
||||
},
|
||||
},
|
||||
emptyLabel: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
modelValue: {
|
||||
default: undefined,
|
||||
},
|
||||
label: {
|
||||
default: null,
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
default: '--kein--',
|
||||
type: String,
|
||||
},
|
||||
def: {
|
||||
required: false,
|
||||
type: Number,
|
||||
default: -1,
|
||||
},
|
||||
name: {
|
||||
required: true,
|
||||
},
|
||||
hint: {},
|
||||
options: {
|
||||
default: function () {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
},
|
||||
emits: ['update:modelValue'],
|
||||
computed: {
|
||||
parsedOptions() {
|
||||
return Array.isArray(this.options)
|
||||
? this.options
|
||||
: map(this.options, (value, key) => {
|
||||
return { name: value, id: key };
|
||||
});
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
if (this.def !== -1 && typeof this.modelValue === 'undefined') {
|
||||
this.$emit('update:modelValue', this.def);
|
||||
return;
|
||||
}
|
||||
const emit = defineEmits(['update:modelValue']);
|
||||
|
||||
if (this.placeholder && typeof this.modelValue === 'undefined') {
|
||||
this.$emit('update:modelValue', null);
|
||||
}
|
||||
const props = defineProps({
|
||||
nullValue: {
|
||||
required: false,
|
||||
default: () => null,
|
||||
},
|
||||
methods: {
|
||||
trigger(v) {
|
||||
this.$emit('update:modelValue', /^[0-9]+$/.test(v.target.value) ? parseInt(v.target.value) : v.target.value ? v.target.value : null);
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
default: function () {
|
||||
return false;
|
||||
},
|
||||
},
|
||||
};
|
||||
id: {},
|
||||
inset: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
size: {
|
||||
default: function () {
|
||||
return 'base';
|
||||
},
|
||||
},
|
||||
emptyLabel: {
|
||||
default: false,
|
||||
type: Boolean,
|
||||
},
|
||||
modelValue: {
|
||||
default: undefined,
|
||||
},
|
||||
label: {
|
||||
default: null,
|
||||
},
|
||||
required: {
|
||||
type: Boolean,
|
||||
default: false,
|
||||
},
|
||||
placeholder: {
|
||||
default: '--kein--',
|
||||
type: String,
|
||||
},
|
||||
def: {
|
||||
required: false,
|
||||
type: Number,
|
||||
default: -1,
|
||||
},
|
||||
name: {
|
||||
required: true,
|
||||
},
|
||||
hint: {},
|
||||
options: {
|
||||
default: function () {
|
||||
return [];
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const parsedOptions = computed(() => {
|
||||
return Array.isArray(props.options)
|
||||
? props.options
|
||||
: map(props.options, (value, key) => {
|
||||
return {name: value, id: key};
|
||||
});
|
||||
});
|
||||
|
||||
const def = ref('iu1Feixah5AeKai3ewooJahjeaegee0eiD4maeth1oul4Hei7u');
|
||||
|
||||
const inner = computed({
|
||||
get: () => {
|
||||
return props.modelValue === props.nullValue ? def.value : props.modelValue;
|
||||
},
|
||||
set: (v) => {
|
||||
emit('update:modelValue', v === def.value ? props.nullValue : v);
|
||||
},
|
||||
});
|
||||
</script>
|
||||
|
|
|
@ -11,9 +11,10 @@ export function useApiIndex(url, siteName) {
|
|||
meta: ref({}),
|
||||
};
|
||||
|
||||
async function reload(resetPage = true) {
|
||||
async function reload(resetPage = true, p = {}) {
|
||||
var params = {
|
||||
page: resetPage ? 1 : inner.meta.value.current_page,
|
||||
...p,
|
||||
};
|
||||
|
||||
var response = (await axios.get(url, {params})).data;
|
||||
|
@ -49,6 +50,10 @@ export function useApiIndex(url, siteName) {
|
|||
return inner.meta.value.can[permission];
|
||||
}
|
||||
|
||||
function toFilterString(data) {
|
||||
return btoa(encodeURIComponent(JSON.stringify(data)));
|
||||
}
|
||||
|
||||
function requestCallback(successMessage, failureMessage) {
|
||||
return {
|
||||
onSuccess: () => {
|
||||
|
@ -85,5 +90,6 @@ export function useApiIndex(url, siteName) {
|
|||
remove,
|
||||
cancel,
|
||||
axios,
|
||||
toFilterString,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -11,6 +11,39 @@
|
|||
</ui-popup>
|
||||
<page-filter breakpoint="lg">
|
||||
<f-multipleselect id="active_columns" v-model="activeColumnsConfig" :options="meta.columns" label="Aktive Spalten" size="sm" name="active_columns"></f-multipleselect>
|
||||
|
||||
<template v-for="(filter, index) in meta.filters">
|
||||
<f-select
|
||||
v-if="filter.base_type === 'CheckboxField'"
|
||||
:id="`filter-field-${index}`"
|
||||
:key="`filter-field-${index}`"
|
||||
v-model="innerFilter.data[filter.key]"
|
||||
:null-value="meta.default_filter_value"
|
||||
:name="`filter-field-${index}`"
|
||||
:options="checkboxFilterOptions"
|
||||
:label="filter.name"
|
||||
></f-select>
|
||||
<f-select
|
||||
v-if="filter.base_type === 'DropdownField'"
|
||||
:id="`filter-field-${index}`"
|
||||
:key="`filter-field-${index}`"
|
||||
v-model="innerFilter.data[filter.key]"
|
||||
:null-value="meta.default_filter_value"
|
||||
:name="`filter-field-${index}`"
|
||||
:options="dropdownFilterOptions(filter)"
|
||||
:label="filter.name"
|
||||
></f-select>
|
||||
<f-select
|
||||
v-if="filter.base_type === 'RadioField'"
|
||||
:id="`filter-field-${index}`"
|
||||
:key="`filter-field-${index}`"
|
||||
v-model="innerFilter.data[filter.key]"
|
||||
:null-value="meta.default_filter_value"
|
||||
:name="`filter-field-${index}`"
|
||||
:options="dropdownFilterOptions(filter)"
|
||||
:label="filter.name"
|
||||
></f-select>
|
||||
</template>
|
||||
</page-filter>
|
||||
<table cellspacing="0" cellpadding="0" border="0" class="custom-table custom-table-sm">
|
||||
<thead>
|
||||
|
@ -35,7 +68,7 @@
|
|||
</template>
|
||||
|
||||
<script setup>
|
||||
import {ref, computed} from 'vue';
|
||||
import {watch, ref, computed} from 'vue';
|
||||
import {useApiIndex} from '../../composables/useApiIndex.js';
|
||||
|
||||
const deleting = ref(null);
|
||||
|
@ -48,7 +81,7 @@ const props = defineProps({
|
|||
},
|
||||
});
|
||||
|
||||
var {meta, data, reload, reloadPage, axios, remove} = useApiIndex(props.url, 'participant');
|
||||
var {meta, data, reload, reloadPage, axios, remove, toFilterString} = useApiIndex(props.url, 'participant');
|
||||
|
||||
const activeColumns = computed(() => meta.value.columns.filter((c) => meta.value.form_meta.active_columns.includes(c.id)));
|
||||
|
||||
|
@ -70,4 +103,30 @@ async function handleDelete() {
|
|||
}
|
||||
|
||||
await reload();
|
||||
|
||||
const innerFilter = ref(JSON.parse(JSON.stringify(meta.value.filter)));
|
||||
|
||||
watch(
|
||||
innerFilter,
|
||||
async function (newValue) {
|
||||
await reload(true, {
|
||||
filter: toFilterString(newValue),
|
||||
});
|
||||
},
|
||||
{deep: true}
|
||||
);
|
||||
|
||||
const checkboxFilterOptions = ref([
|
||||
{id: true, name: 'Ja'},
|
||||
{id: false, name: 'Nein'},
|
||||
]);
|
||||
|
||||
function dropdownFilterOptions(filter) {
|
||||
return [
|
||||
{id: null, name: 'keine Auswahl'},
|
||||
...filter.options.map((f) => {
|
||||
return {id: f, name: f};
|
||||
}),
|
||||
];
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -2,9 +2,11 @@
|
|||
|
||||
namespace Tests\Feature\Form;
|
||||
|
||||
use App\Form\Fields\CheckboxField;
|
||||
use App\Form\Fields\TextField;
|
||||
use App\Form\Models\Form;
|
||||
use App\Form\Models\Participant;
|
||||
use App\Form\Scopes\ParticipantFilterScope;
|
||||
use App\Group;
|
||||
use Carbon\Carbon;
|
||||
use Illuminate\Foundation\Testing\DatabaseTransactions;
|
||||
|
@ -56,6 +58,75 @@ class ParticipantIndexActionTest extends FormTestCase
|
|||
->assertJsonPath('meta.form_meta.sorting', ['vorname', 'asc']);
|
||||
}
|
||||
|
||||
public function testItShowsEmptyFilters(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()->fields([$this->checkboxField('check')->name('Checked')])->create();
|
||||
|
||||
$this->callFilter('form.participant.index', [], ['form' => $form])
|
||||
->assertOk()
|
||||
->assertJsonPath('meta.filters.0.name', 'Checked')
|
||||
->assertJsonPath('meta.filters.0.key', 'check')
|
||||
->assertJsonPath('meta.filters.0.base_type', 'CheckboxField')
|
||||
->assertJsonPath('meta.default_filter_value', ParticipantFilterScope::$nan);
|
||||
|
||||
$this->callFilter('form.participant.index', ['data' => ['check' => null]], ['form' => $form])->assertHasJsonPath('meta.filter.data.check')->assertJsonPath('meta.filter.data.check', null);
|
||||
$this->callFilter('form.participant.index', ['data' => ['check' => 'A']], ['form' => $form])->assertJsonPath('meta.filter.data.check', 'A');
|
||||
$this->callFilter('form.participant.index', ['data' => []], ['form' => $form])->assertJsonPath('meta.filter.data.check', ParticipantFilterScope::$nan);
|
||||
}
|
||||
|
||||
public function testItFiltersParticipantsByCheckboxValue(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()->fields([$this->checkboxField('check')])
|
||||
->has(Participant::factory()->data(['check' => true])->count(1))
|
||||
->has(Participant::factory()->data(['check' => false])->count(2))
|
||||
->create();
|
||||
|
||||
$this->callFilter('form.participant.index', ['data' => ['check' => ParticipantFilterScope::$nan]], ['form' => $form])
|
||||
->assertJsonCount(3, 'data');
|
||||
$this->callFilter('form.participant.index', ['data' => ['check' => true]], ['form' => $form])
|
||||
->assertJsonCount(1, 'data');
|
||||
$this->callFilter('form.participant.index', ['data' => ['check' => false]], ['form' => $form])
|
||||
->assertJsonCount(2, 'data');
|
||||
}
|
||||
|
||||
public function testItFiltersParticipantsByDropdownValue(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()->fields([$this->dropdownField('drop')->options(['A', 'B'])])
|
||||
->has(Participant::factory()->data(['drop' => null])->count(1))
|
||||
->has(Participant::factory()->data(['drop' => 'A'])->count(2))
|
||||
->has(Participant::factory()->data(['drop' => 'B'])->count(4))
|
||||
->create();
|
||||
|
||||
$this->callFilter('form.participant.index', ['data' => ['drop' => ParticipantFilterScope::$nan]], ['form' => $form])
|
||||
->assertJsonCount(7, 'data');
|
||||
$this->callFilter('form.participant.index', ['data' => ['drop' => null]], ['form' => $form])
|
||||
->assertJsonCount(1, 'data');
|
||||
$this->callFilter('form.participant.index', ['data' => ['drop' => 'A']], ['form' => $form])
|
||||
->assertJsonCount(2, 'data');
|
||||
$this->callFilter('form.participant.index', ['data' => ['drop' => 'B']], ['form' => $form])
|
||||
->assertJsonCount(4, 'data');
|
||||
}
|
||||
|
||||
public function testItFiltersParticipantsByRadioValue(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
$form = Form::factory()->fields([$this->radioField('drop')->options(['A', 'B'])])
|
||||
->has(Participant::factory()->data(['drop' => null])->count(1))
|
||||
->has(Participant::factory()->data(['drop' => 'A'])->count(2))
|
||||
->has(Participant::factory()->data(['drop' => 'B'])->count(4))
|
||||
->create();
|
||||
|
||||
$this->callFilter('form.participant.index', ['data' => ['drop' => ParticipantFilterScope::$nan]], ['form' => $form])
|
||||
->assertJsonCount(7, 'data');
|
||||
$this->callFilter('form.participant.index', ['data' => ['drop' => 'A']], ['form' => $form])
|
||||
->assertJsonCount(2, 'data');
|
||||
$this->callFilter('form.participant.index', ['data' => ['drop' => 'B']], ['form' => $form])
|
||||
->assertJsonCount(4, 'data');
|
||||
}
|
||||
|
||||
public function testItShowsOnlyRootMembers(): void
|
||||
{
|
||||
$this->login()->loginNami()->withoutExceptionHandling();
|
||||
|
|
|
@ -8,6 +8,7 @@ use App\Setting\NamiSettings;
|
|||
use App\User;
|
||||
use Illuminate\Foundation\Testing\TestCase as BaseTestCase;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
use Illuminate\Support\Arr;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Testing\AssertableJsonString;
|
||||
use Illuminate\Testing\TestResponse;
|
||||
|
@ -153,5 +154,13 @@ abstract class TestCase extends BaseTestCase
|
|||
|
||||
return $this;
|
||||
});
|
||||
|
||||
TestResponse::macro('assertHasJsonPath', function (string $path) {
|
||||
/** @var TestResponse */
|
||||
$response = $this;
|
||||
Assert::assertTrue(Arr::has($response->json(), $path), 'Failed that key ' . $path . ' is in Response.');
|
||||
|
||||
return $this;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue