mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-22 18:03:37 +02:00
Added the very basic foundations for a filter system
This commit is contained in:
parent
ef389dcc15
commit
f9d945c4c7
15 changed files with 460 additions and 5 deletions
|
@ -1,6 +1,6 @@
|
|||
twig:
|
||||
default_path: '%kernel.project_dir%/templates'
|
||||
form_themes: ['bootstrap_5_horizontal_layout.html.twig', 'Form/extendedBootstrap4_layout.html.twig', 'Form/permissionLayout.html.twig' ]
|
||||
form_themes: ['bootstrap_5_horizontal_layout.html.twig', 'Form/extendedBootstrap4_layout.html.twig', 'Form/permissionLayout.html.twig', 'Form/FilterTypesLayout.html.twig']
|
||||
|
||||
paths:
|
||||
'%kernel.project_dir%/assets/css': css
|
||||
|
|
|
@ -42,12 +42,14 @@ declare(strict_types=1);
|
|||
|
||||
namespace App\Controller;
|
||||
|
||||
use App\DataTables\Filters\PartFilter;
|
||||
use App\DataTables\PartsDataTable;
|
||||
use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Footprint;
|
||||
use App\Entity\Parts\Manufacturer;
|
||||
use App\Entity\Parts\Storelocation;
|
||||
use App\Entity\Parts\Supplier;
|
||||
use App\Form\Filters\PartFilterType;
|
||||
use App\Services\Parts\PartsTableActionHandler;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Omines\DataTablesBundle\DataTableFactory;
|
||||
|
@ -245,11 +247,11 @@ class PartListsController extends AbstractController
|
|||
'regex' => $request->query->getBoolean('regex'),
|
||||
];
|
||||
|
||||
|
||||
$table = $dataTable->createFromType(PartsDataTable::class, [
|
||||
'search' => $search,
|
||||
'search_options' => $search_options,
|
||||
])
|
||||
->handleRequest($request);
|
||||
])->handleRequest($request);
|
||||
|
||||
if ($table->isCallback()) {
|
||||
return $table->getResponse();
|
||||
|
@ -258,6 +260,7 @@ class PartListsController extends AbstractController
|
|||
return $this->render('Parts/lists/search_list.html.twig', [
|
||||
'datatable' => $table,
|
||||
'keyword' => $search,
|
||||
'filterForm' => $filterForm->createView()
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -268,13 +271,22 @@ class PartListsController extends AbstractController
|
|||
*/
|
||||
public function showAll(Request $request, DataTableFactory $dataTable)
|
||||
{
|
||||
$table = $dataTable->createFromType(PartsDataTable::class)
|
||||
|
||||
$formRequest = clone $request;
|
||||
$formRequest->setMethod('GET');
|
||||
$filter = new PartFilter();
|
||||
$filterForm = $this->createForm(PartFilterType::class, $filter, ['method' => 'GET']);
|
||||
$filterForm->handleRequest($formRequest);
|
||||
|
||||
$table = $dataTable->createFromType(PartsDataTable::class, [
|
||||
'filter' => $filter,
|
||||
])
|
||||
->handleRequest($request);
|
||||
|
||||
if ($table->isCallback()) {
|
||||
return $table->getResponse();
|
||||
}
|
||||
|
||||
return $this->render('Parts/lists/all_list.html.twig', ['datatable' => $table]);
|
||||
return $this->render('Parts/lists/all_list.html.twig', ['datatable' => $table, 'filterForm' => $filterForm->createView()]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\DataTables\Filters\Constraints;
|
||||
|
||||
use App\DataTables\Filters\FilterInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
abstract class AbstractSimpleConstraint implements FilterInterface
|
||||
{
|
||||
use FilterTrait;
|
||||
|
||||
/**
|
||||
* @var string The property where this BooleanConstraint should apply to
|
||||
*/
|
||||
protected $property;
|
||||
|
||||
/**
|
||||
* @var string
|
||||
*/
|
||||
protected $identifier;
|
||||
|
||||
|
||||
public function __construct(string $property, string $identifier = null)
|
||||
{
|
||||
$this->property = $property;
|
||||
$this->identifier = $identifier ?? $this->generateParameterIdentifier($property);
|
||||
}
|
||||
}
|
48
src/DataTables/Filters/Constraints/BooleanConstraint.php
Normal file
48
src/DataTables/Filters/Constraints/BooleanConstraint.php
Normal file
|
@ -0,0 +1,48 @@
|
|||
<?php
|
||||
|
||||
namespace App\DataTables\Filters\Constraints;
|
||||
|
||||
use App\DataTables\Filters\FilterInterface;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
class BooleanConstraint extends AbstractSimpleConstraint
|
||||
{
|
||||
/** @var bool|null The value of our constraint */
|
||||
protected $value;
|
||||
|
||||
|
||||
public function __construct(string $property, string $identifier = null, ?bool $default_value = null)
|
||||
{
|
||||
parent::__construct($property, $identifier, $default_value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the value of this constraint. Null means "don't filter", true means "filter for true", false means "filter for false".
|
||||
* @return bool|null
|
||||
*/
|
||||
public function getValue(): ?bool
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the value of this constraint. Null means "don't filter", true means "filter for true", false means "filter for false".
|
||||
* @param bool|null $value
|
||||
*/
|
||||
public function setValue(?bool $value): void
|
||||
{
|
||||
$this->value = $value;
|
||||
}
|
||||
|
||||
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder): void
|
||||
{
|
||||
//Do not apply a filter if value is null (filter is set to ignore)
|
||||
if($this->value === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, '=', $this->value);
|
||||
}
|
||||
}
|
33
src/DataTables/Filters/Constraints/FilterTrait.php
Normal file
33
src/DataTables/Filters/Constraints/FilterTrait.php
Normal file
|
@ -0,0 +1,33 @@
|
|||
<?php
|
||||
|
||||
namespace App\DataTables\Filters\Constraints;
|
||||
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
trait FilterTrait
|
||||
{
|
||||
|
||||
/**
|
||||
* Generates a parameter identifier that can be used for the given property. It gives random results, to be unique, so you have to cache it.
|
||||
* @param string $property
|
||||
* @return string
|
||||
*/
|
||||
protected function generateParameterIdentifier(string $property): string
|
||||
{
|
||||
return str_replace('.', '_', $property) . '_' . uniqid("", false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Adds a simple constraint in the form of (property OPERATOR value) (e.g. "part.name = :name") to the given query builder.
|
||||
* @param QueryBuilder $queryBuilder
|
||||
* @param string $property
|
||||
* @param string $comparison_operator
|
||||
* @param mixed $value
|
||||
* @return void
|
||||
*/
|
||||
protected function addSimpleAndConstraint(QueryBuilder $queryBuilder, string $property, string $parameterIdentifier, string $comparison_operator, $value): void
|
||||
{
|
||||
$queryBuilder->andWhere(sprintf("%s %s :%s", $property, $comparison_operator, $parameterIdentifier));
|
||||
$queryBuilder->setParameter($parameterIdentifier, $value);
|
||||
}
|
||||
}
|
110
src/DataTables/Filters/Constraints/NumberConstraint.php
Normal file
110
src/DataTables/Filters/Constraints/NumberConstraint.php
Normal file
|
@ -0,0 +1,110 @@
|
|||
<?php
|
||||
|
||||
namespace App\DataTables\Filters\Constraints;
|
||||
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use \RuntimeException;
|
||||
|
||||
class NumberConstraint extends AbstractSimpleConstraint
|
||||
{
|
||||
public const ALLOWED_OPERATOR_VALUES = ['=', '!=', '<', '>', '<=', '>=', 'BETWEEN'];
|
||||
|
||||
|
||||
/**
|
||||
* The value1 used for comparison (this is the main one used for all mono-value comparisons)
|
||||
* @var float|null
|
||||
*/
|
||||
protected $value1;
|
||||
|
||||
/**
|
||||
* The second value used when operator is RANGE; this is the upper bound of the range
|
||||
* @var float|null
|
||||
*/
|
||||
protected $value2;
|
||||
|
||||
/**
|
||||
* @var string The operator to use
|
||||
*/
|
||||
protected $operator;
|
||||
|
||||
/**
|
||||
* @return float|mixed|null
|
||||
*/
|
||||
public function getValue1()
|
||||
{
|
||||
return $this->value1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float|mixed|null $value1
|
||||
*/
|
||||
public function setValue1($value1): void
|
||||
{
|
||||
$this->value1 = $value1;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return float|mixed|null
|
||||
*/
|
||||
public function getValue2()
|
||||
{
|
||||
return $this->value2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param float|mixed|null $value2
|
||||
*/
|
||||
public function setValue2($value2): void
|
||||
{
|
||||
$this->value2 = $value2;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return mixed|string
|
||||
*/
|
||||
public function getOperator()
|
||||
{
|
||||
return $this->operator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param mixed|string $operator
|
||||
*/
|
||||
public function setOperator($operator): void
|
||||
{
|
||||
$this->operator = $operator;
|
||||
}
|
||||
|
||||
|
||||
public function __construct(string $property, string $identifier = null, $value1 = null, $operator = '>', $value2 = null)
|
||||
{
|
||||
parent::__construct($property, $identifier);
|
||||
$this->value1 = $value1;
|
||||
$this->value2 = $value2;
|
||||
$this->operator = $operator;
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder): void
|
||||
{
|
||||
//If no value is provided then we do not apply a filter
|
||||
if ($this->value1 === null) {
|
||||
return;
|
||||
}
|
||||
|
||||
//Ensure we have an valid operator
|
||||
if(!in_array($this->operator, self::ALLOWED_OPERATOR_VALUES, true)) {
|
||||
throw new \InvalidArgumentException('Invalid operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES));
|
||||
}
|
||||
|
||||
if ($this->operator !== 'BETWEEN') {
|
||||
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, $this->operator, $this->value1);
|
||||
} else {
|
||||
if ($this->value2 === null) {
|
||||
throw new RuntimeException("Cannot use operator BETWEEN without value2!");
|
||||
}
|
||||
|
||||
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier . '1', '>=', $this->value1);
|
||||
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier . '2', '<=', $this->value2);
|
||||
}
|
||||
}
|
||||
}
|
16
src/DataTables/Filters/FilterInterface.php
Normal file
16
src/DataTables/Filters/FilterInterface.php
Normal file
|
@ -0,0 +1,16 @@
|
|||
<?php
|
||||
|
||||
namespace App\DataTables\Filters;
|
||||
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
interface FilterInterface
|
||||
{
|
||||
|
||||
/**
|
||||
* Apply the given filter to the given query builder on the given property
|
||||
* @param QueryBuilder $queryBuilder
|
||||
* @return void
|
||||
*/
|
||||
public function apply(QueryBuilder $queryBuilder): void;
|
||||
}
|
54
src/DataTables/Filters/PartFilter.php
Normal file
54
src/DataTables/Filters/PartFilter.php
Normal file
|
@ -0,0 +1,54 @@
|
|||
<?php
|
||||
|
||||
namespace App\DataTables\Filters;
|
||||
|
||||
use App\DataTables\Filters\Constraints\BooleanConstraint;
|
||||
use App\DataTables\Filters\Constraints\NumberConstraint;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
class PartFilter implements FilterInterface
|
||||
{
|
||||
/** @var BooleanConstraint */
|
||||
protected $favorite;
|
||||
|
||||
/** @var BooleanConstraint */
|
||||
protected $needsReview;
|
||||
|
||||
/** @var NumberConstraint */
|
||||
protected $mass;
|
||||
|
||||
/**
|
||||
* @return BooleanConstraint|false
|
||||
*/
|
||||
public function getFavorite()
|
||||
{
|
||||
return $this->favorite;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return BooleanConstraint
|
||||
*/
|
||||
public function getNeedsReview(): BooleanConstraint
|
||||
{
|
||||
return $this->needsReview;
|
||||
}
|
||||
|
||||
public function getMass(): NumberConstraint
|
||||
{
|
||||
return $this->mass;
|
||||
}
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
$this->favorite = new BooleanConstraint('part.favorite');
|
||||
$this->needsReview = new BooleanConstraint('part.needs_review');
|
||||
$this->mass = new NumberConstraint('part.mass');
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder): void
|
||||
{
|
||||
$this->favorite->apply($queryBuilder);
|
||||
$this->needsReview->apply($queryBuilder);
|
||||
$this->mass->apply($queryBuilder);
|
||||
}
|
||||
}
|
|
@ -48,6 +48,7 @@ use App\DataTables\Column\LocaleDateTimeColumn;
|
|||
use App\DataTables\Column\MarkdownColumn;
|
||||
use App\DataTables\Column\PartAttachmentsColumn;
|
||||
use App\DataTables\Column\TagsColumn;
|
||||
use App\DataTables\Filters\PartFilter;
|
||||
use App\Entity\Parts\Category;
|
||||
use App\Entity\Parts\Footprint;
|
||||
use App\Entity\Parts\Manufacturer;
|
||||
|
@ -108,6 +109,7 @@ final class PartsDataTable implements DataTableTypeInterface
|
|||
'supplier' => null,
|
||||
'tag' => null,
|
||||
'search' => null,
|
||||
'filter' => null
|
||||
]);
|
||||
|
||||
$optionsResolver->setAllowedTypes('category', ['null', Category::class]);
|
||||
|
@ -351,6 +353,13 @@ final class PartsDataTable implements DataTableTypeInterface
|
|||
|
||||
private function buildCriteria(QueryBuilder $builder, array $options): void
|
||||
{
|
||||
|
||||
if (isset($options['filter']) && $options['filter'] instanceof PartFilter) {
|
||||
$filter = $options['filter'];
|
||||
|
||||
$filter->apply($builder);
|
||||
}
|
||||
|
||||
if (isset($options['category'])) {
|
||||
$category = $options['category'];
|
||||
$list = $this->treeBuilder->typeToNodesList(Category::class, $category);
|
||||
|
|
28
src/Form/Filters/Constraints/BooleanConstraintType.php
Normal file
28
src/Form/Filters/Constraints/BooleanConstraintType.php
Normal file
|
@ -0,0 +1,28 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Filters\Constraints;
|
||||
|
||||
use App\DataTables\Filters\Constraints\BooleanConstraint;
|
||||
use App\Form\Type\TriStateCheckboxType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class BooleanConstraintType extends AbstractType
|
||||
{
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'compound' => true,
|
||||
'data_class' => BooleanConstraint::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$builder->add('value', TriStateCheckboxType::class, [
|
||||
'label' => $options['label'],
|
||||
'required' => false,
|
||||
]);
|
||||
}
|
||||
}
|
57
src/Form/Filters/Constraints/NumberConstraintType.php
Normal file
57
src/Form/Filters/Constraints/NumberConstraintType.php
Normal file
|
@ -0,0 +1,57 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Filters\Constraints;
|
||||
|
||||
use App\DataTables\Filters\Constraints\NumberConstraint;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\NumberType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class NumberConstraintType extends AbstractType
|
||||
{
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'compound' => true,
|
||||
'data_class' => NumberConstraint::class,
|
||||
]);
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||
{
|
||||
$choices = [
|
||||
'=' => '=',
|
||||
'!=' => '!=',
|
||||
'<' => '<',
|
||||
'>' => '>',
|
||||
'<=' => '<=',
|
||||
'>=' => '>=',
|
||||
'BETWEEN' => 'BETWEEN',
|
||||
];
|
||||
|
||||
$builder->add('value1', NumberType::class, [
|
||||
'label' => 'filter.number_constraint.value1',
|
||||
'attr' => [
|
||||
'placeholder' => 'filter.number_constraint.value1',
|
||||
],
|
||||
'required' => false,
|
||||
'html5' => true,
|
||||
]);
|
||||
$builder->add('value2', NumberType::class, [
|
||||
'label' => 'filter.number_constraint.value2',
|
||||
'attr' => [
|
||||
'placeholder' => 'filter.number_constraint.value2',
|
||||
],
|
||||
'required' => false,
|
||||
'html5' => true,
|
||||
]);
|
||||
$builder->add('operator', ChoiceType::class, [
|
||||
'label' => 'filter.number_constraint.operator',
|
||||
'choices' => $choices,
|
||||
]);
|
||||
}
|
||||
}
|
43
src/Form/Filters/PartFilterType.php
Normal file
43
src/Form/Filters/PartFilterType.php
Normal file
|
@ -0,0 +1,43 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Filters;
|
||||
|
||||
use App\DataTables\Filters\PartFilter;
|
||||
use App\Form\Filters\Constraints\BooleanConstraintType;
|
||||
use App\Form\Filters\Constraints\NumberConstraintType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\SubmitButton;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class PartFilterType extends AbstractType
|
||||
{
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'compound' => true,
|
||||
'data_class' => PartFilter::class,
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->add('favorite', BooleanConstraintType::class, [
|
||||
'label' => 'part.edit.is_favorite'
|
||||
]);
|
||||
|
||||
$builder->add('needsReview', BooleanConstraintType::class, [
|
||||
'label' => 'part.edit.needs_review'
|
||||
]);
|
||||
|
||||
$builder->add('mass', NumberConstraintType::class, [
|
||||
'label' => 'part.edit.mass'
|
||||
]);
|
||||
|
||||
$builder->add('submit', SubmitType::class, [
|
||||
'label' => 'Update',
|
||||
]);
|
||||
}
|
||||
}
|
7
templates/Form/FilterTypesLayout.html.twig
Normal file
7
templates/Form/FilterTypesLayout.html.twig
Normal file
|
@ -0,0 +1,7 @@
|
|||
{% block number_constraint_widget %}
|
||||
<div class="input-group">
|
||||
{{ form_widget(form.operator) }}
|
||||
{{ form_widget(form.value1) }}
|
||||
{{ form_widget(form.value2) }}
|
||||
</div>
|
||||
{% endblock %}
|
8
templates/Parts/lists/_filter.html.twig
Normal file
8
templates/Parts/lists/_filter.html.twig
Normal file
|
@ -0,0 +1,8 @@
|
|||
<div class="card mb-4">
|
||||
<div class="card-header">Filter</div>
|
||||
<div class="card-body">
|
||||
{{ form_start(filterForm) }}
|
||||
|
||||
{{ form_end(filterForm) }}
|
||||
</div>
|
||||
</div>
|
|
@ -6,6 +6,8 @@
|
|||
|
||||
{% block content %}
|
||||
|
||||
{% include "Parts/lists/_filter.html.twig" %}
|
||||
|
||||
{% include "Parts/lists/_action_bar.html.twig" with {'url_options': {}} %}
|
||||
{% include "Parts/lists/_parts_list.html.twig" %}
|
||||
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue