feat: multiselect field for filtering

This commit is contained in:
Björn Fromme
2024-07-15 18:25:14 +02:00
parent 1cb0227d1e
commit 7d84f321da
9 changed files with 151 additions and 31 deletions
@@ -0,0 +1,30 @@
import { Controller } from '@hotwired/stimulus'
export default class extends Controller {
static targets = [ 'label', 'dropdown', 'choice' ]
static values = { placeholder: String }
connect() {
this.update()
}
toggle() {
this.dropdownTarget.classList.toggle('hidden')
}
update() {
let labels = []
this.choiceTargets.forEach((choice) => {
if (choice.checked) {
labels.push(choice.dataset.label)
}
this.labelTarget.innerText = labels.join(', ')
})
if (0 === labels.length) {
this.labelTarget.innerText = this.placeholderValue
}
}
}
+4 -4
View File
@@ -80,11 +80,11 @@ class AssignmentFilterType extends AbstractType
if (0 < count($options['job_profiles'])) { if (0 < count($options['job_profiles'])) {
$builder $builder
->add('jobProfile', EntityType::class, [ ->add('jobProfiles', MultiselectEntityType::class, [
'label' => 'Job-Profil', 'label' => 'Job-Profil',
'class' => JobProfile::class, 'class' => JobProfile::class,
'required' => false, 'required' => false,
'placeholder' => 'nicht filtern', 'empty_label' => 'nicht filtern',
'choice_label' => 'name', 'choice_label' => 'name',
'choices' => $options['job_profiles'], 'choices' => $options['job_profiles'],
]) ])
@@ -96,10 +96,10 @@ class AssignmentFilterType extends AbstractType
$choices[$item->getHotel()] = $item->getHotelCode(); $choices[$item->getHotel()] = $item->getHotelCode();
} }
$builder $builder
->add('hotel', ChoiceType::class, [ ->add('hotels', MultiselectType::class, [
'label' => 'Haus', 'label' => 'Haus',
'required' => false, 'required' => false,
'placeholder' => 'nicht filtern', 'empty_label' => 'nicht filtern',
'choices' => $choices, 'choices' => $choices,
]) ])
; ;
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace App\Form;
use Symfony\Bridge\Doctrine\Form\Type\EntityType;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class MultiselectEntityType extends AbstractType
{
public function getParent(): string
{
return EntityType::class;
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['empty_label'] = $options['empty_label'];
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'multiple' => true,
'expanded' => true,
'empty_label' => null,
]);
}
public function getBlockPrefix(): string
{
return 'multiselect';
}
}
+36
View File
@@ -0,0 +1,36 @@
<?php
namespace App\Form;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\FormInterface;
use Symfony\Component\Form\FormView;
use Symfony\Component\OptionsResolver\OptionsResolver;
class MultiselectType extends AbstractType
{
public function getParent(): string
{
return ChoiceType::class;
}
public function buildView(FormView $view, FormInterface $form, array $options): void
{
$view->vars['empty_label'] = $options['empty_label'];
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'multiple' => true,
'expanded' => true,
'empty_label' => null,
]);
}
public function getBlockPrefix(): string
{
return 'multiselect';
}
}
+10 -10
View File
@@ -14,8 +14,8 @@ class AssignmentFilterDto extends AbstractFilterDto
protected ?\DateTimeImmutable $dateFrom = null; protected ?\DateTimeImmutable $dateFrom = null;
protected ?\DateTimeImmutable $dateTo = null; protected ?\DateTimeImmutable $dateTo = null;
protected ?JobProfile $jobProfile = null; protected array $jobProfiles = [];
protected ?string $hotel = null; protected array $hotels = [];
protected ?int $duration = null; protected ?int $duration = null;
protected ?string $status = null; protected ?string $status = null;
protected bool $includePast = false; protected bool $includePast = false;
@@ -44,26 +44,26 @@ class AssignmentFilterDto extends AbstractFilterDto
return $this; return $this;
} }
public function getJobProfile(): ?JobProfile public function getJobProfiles(): array
{ {
return $this->jobProfile; return $this->jobProfiles;
} }
public function setJobProfile(?JobProfile $jobProfile): static public function setJobProfiles(array $jobProfiles): static
{ {
$this->jobProfile = $jobProfile; $this->jobProfiles = $jobProfiles;
return $this; return $this;
} }
public function getHotel(): ?string public function getHotels(): ?array
{ {
return $this->hotel; return $this->hotels;
} }
public function setHotel(?string $hotel): static public function setHotels(array $hotels): static
{ {
$this->hotel = $hotel; $this->hotels = $hotels;
return $this; return $this;
} }
+6 -6
View File
@@ -154,17 +154,17 @@ class AssignmentRepository extends ServiceEntityRepository
; ;
} }
if (null !== $jobProfile = $filterDto->getJobProfile()) { if ($jobProfiles = $filterDto->getJobProfiles()) {
$qb $qb
->andWhere($qb->expr()->eq('assignment.jobProfile', ':jobProfile')) ->andWhere($qb->expr()->in('assignment.jobProfile', ':jobProfiles'))
->setParameter('jobProfile', $jobProfile) ->setParameter('jobProfiles', $jobProfiles)
; ;
} }
if (null !== $hotelCode = $filterDto->getHotel()) { if ($hotelCodes = $filterDto->getHotels()) {
$qb $qb
->andWhere($qb->expr()->eq('destination.hotelCode', ':hotelCode')) ->andWhere($qb->expr()->in('destination.hotelCode', ':hotelCodes'))
->setParameter('hotelCode', $hotelCode) ->setParameter('hotelCodes', $hotelCodes)
; ;
} }
+10 -8
View File
@@ -59,16 +59,16 @@ class AssignmentFilterHandler
if (isset($data['date_to'])) { if (isset($data['date_to'])) {
$filterDto->setDateTo($data['date_to']); $filterDto->setDateTo($data['date_to']);
} }
if (isset($data['job_profile']) && 0 < (int) $data['job_profile']) { if (isset($data['job_profiles']) && 0 < count($data['job_profiles'])) {
$jobProfile = $this $jobProfiles = $this
->entityManager ->entityManager
->getRepository(JobProfile::class) ->getRepository(JobProfile::class)
->find($data['job_profile']) ->findBy(['id' => $data['job_profiles']])
; ;
$filterDto->setJobProfile($jobProfile); $filterDto->setJobProfiles($jobProfiles);
} }
if (isset($data['hotel'])) { if (isset($data['hotels']) && 0 < count($data['hotels'])) {
$filterDto->setHotel($data['hotel']); $filterDto->setHotels($data['hotels']);
} }
if (isset($data['duration']) && 0 < (int) $data['duration']) { if (isset($data['duration']) && 0 < (int) $data['duration']) {
$filterDto->setDuration($data['duration']); $filterDto->setDuration($data['duration']);
@@ -88,8 +88,10 @@ class AssignmentFilterHandler
$this->getSession()->set($this->namespace, [ $this->getSession()->set($this->namespace, [
'date_from' => $filterDto->getDateFrom(), 'date_from' => $filterDto->getDateFrom(),
'date_to' => $filterDto->getDateTo(), 'date_to' => $filterDto->getDateTo(),
'job_profile' => $filterDto->getJobProfile()?->getId(), 'job_profiles' => array_map(function (JobProfile $jobProfile) {
'hotel' => $filterDto->getHotel(), return $jobProfile->getId();
}, $filterDto->getJobProfiles()),
'hotels' => $filterDto->getHotels(),
'duration' => $filterDto->getDuration(), 'duration' => $filterDto->getDuration(),
'status' => $filterDto->getStatus(), 'status' => $filterDto->getStatus(),
'include_past' => $filterDto->isIncludePast(), 'include_past' => $filterDto->isIncludePast(),
@@ -10,8 +10,8 @@
{% endif %} {% endif %}
{{ form_row(filterForm.dateFrom) }} {{ form_row(filterForm.dateFrom) }}
{{ form_row(filterForm.dateTo) }} {{ form_row(filterForm.dateTo) }}
{{ form_row(filterForm.jobProfile) }} {{ form_row(filterForm.jobProfiles) }}
{{ form_row(filterForm.hotel) }} {{ form_row(filterForm.hotels) }}
{{ form_row(filterForm.duration) }} {{ form_row(filterForm.duration) }}
{{ form_row(filterForm.includePast) }} {{ form_row(filterForm.includePast) }}
</div> </div>
+17 -1
View File
@@ -216,4 +216,20 @@
{{ icon('search', 'w-4 h-4 text-gray-700') }} {{ icon('search', 'w-4 h-4 text-gray-700') }}
</div> </div>
</div> </div>
{%- endblock autocomplete_widget %} {%- endblock autocomplete_widget -%}
{%- block multiselect_widget -%}
<div class="relative" {{ stimulus_controller('multiselect', { 'placeholder': form.vars.empty_label }) }}>
<div {{ stimulus_action('multiselect', 'toggle', 'click')}}
role="button"
class="relative rounded-md border border-gray-300 flex items-center space-x-2 px-3">
<div {{ stimulus_target('multiselect', 'label') }} class="flex-1 h-8 py-1.5 text-gray-900 text-sm overflow-hidden whitespace-nowrap"></div>
{{ icon('dots', 'w-4 h-4') }}
</div>
<div {{ stimulus_target('multiselect', 'dropdown')}} class="absolute z-10 top-full -mt-1.5 left-0 inset-x-0 px-3 py-2 bg-white shadow-lg rounded-md rounded-t-none border border-gray-300 border-t-0 hidden">
{% for item in form.children %}
{{ form_row(item, { 'attr': { 'data-action': 'multiselect#update', 'data-multiselect-target': 'choice', 'data-label': item.vars.label } }) }}
{% endfor %}
</div>
</div>
{%- endblock multiselect_widget -%}