diff --git a/config/packages/twig.yaml b/config/packages/twig.yaml index 5fdd5bec..0c4492af 100644 --- a/config/packages/twig.yaml +++ b/config/packages/twig.yaml @@ -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 diff --git a/src/Controller/PartListsController.php b/src/Controller/PartListsController.php index 32719591..070db537 100644 --- a/src/Controller/PartListsController.php +++ b/src/Controller/PartListsController.php @@ -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()]); } } diff --git a/src/DataTables/Filters/Constraints/AbstractSimpleConstraint.php b/src/DataTables/Filters/Constraints/AbstractSimpleConstraint.php new file mode 100644 index 00000000..e45f1909 --- /dev/null +++ b/src/DataTables/Filters/Constraints/AbstractSimpleConstraint.php @@ -0,0 +1,28 @@ +property = $property; + $this->identifier = $identifier ?? $this->generateParameterIdentifier($property); + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/BooleanConstraint.php b/src/DataTables/Filters/Constraints/BooleanConstraint.php new file mode 100644 index 00000000..4051ad49 --- /dev/null +++ b/src/DataTables/Filters/Constraints/BooleanConstraint.php @@ -0,0 +1,48 @@ +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); + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/FilterTrait.php b/src/DataTables/Filters/Constraints/FilterTrait.php new file mode 100644 index 00000000..3823ef89 --- /dev/null +++ b/src/DataTables/Filters/Constraints/FilterTrait.php @@ -0,0 +1,33 @@ +andWhere(sprintf("%s %s :%s", $property, $comparison_operator, $parameterIdentifier)); + $queryBuilder->setParameter($parameterIdentifier, $value); + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/NumberConstraint.php b/src/DataTables/Filters/Constraints/NumberConstraint.php new file mode 100644 index 00000000..410b6f63 --- /dev/null +++ b/src/DataTables/Filters/Constraints/NumberConstraint.php @@ -0,0 +1,110 @@ +', '<=', '>=', '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); + } + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/FilterInterface.php b/src/DataTables/Filters/FilterInterface.php new file mode 100644 index 00000000..0a13a4aa --- /dev/null +++ b/src/DataTables/Filters/FilterInterface.php @@ -0,0 +1,16 @@ +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); + } +} diff --git a/src/DataTables/PartsDataTable.php b/src/DataTables/PartsDataTable.php index db81b348..de06a09f 100644 --- a/src/DataTables/PartsDataTable.php +++ b/src/DataTables/PartsDataTable.php @@ -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); diff --git a/src/Form/Filters/Constraints/BooleanConstraintType.php b/src/Form/Filters/Constraints/BooleanConstraintType.php new file mode 100644 index 00000000..0ad27372 --- /dev/null +++ b/src/Form/Filters/Constraints/BooleanConstraintType.php @@ -0,0 +1,28 @@ +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, + ]); + } +} \ No newline at end of file diff --git a/src/Form/Filters/Constraints/NumberConstraintType.php b/src/Form/Filters/Constraints/NumberConstraintType.php new file mode 100644 index 00000000..2f006ecf --- /dev/null +++ b/src/Form/Filters/Constraints/NumberConstraintType.php @@ -0,0 +1,57 @@ +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, + ]); + } +} \ No newline at end of file diff --git a/src/Form/Filters/PartFilterType.php b/src/Form/Filters/PartFilterType.php new file mode 100644 index 00000000..1129e9dd --- /dev/null +++ b/src/Form/Filters/PartFilterType.php @@ -0,0 +1,43 @@ +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', + ]); + } +} \ No newline at end of file diff --git a/templates/Form/FilterTypesLayout.html.twig b/templates/Form/FilterTypesLayout.html.twig new file mode 100644 index 00000000..9db0c3bd --- /dev/null +++ b/templates/Form/FilterTypesLayout.html.twig @@ -0,0 +1,7 @@ +{% block number_constraint_widget %} +