Added an filter constraint based on part tags.

This commit is contained in:
Jan Böhmer 2022-08-21 23:01:10 +02:00
parent 4d3ff7d7b5
commit 4ba58cc621
7 changed files with 234 additions and 4 deletions

View file

@ -0,0 +1,137 @@
<?php
namespace App\DataTables\Filters\Constraints\Part;
use App\DataTables\Filters\Constraints\AbstractConstraint;
use Doctrine\ORM\Query\Expr;
use Doctrine\ORM\QueryBuilder;
class TagsConstraint extends AbstractConstraint
{
public const ALLOWED_OPERATOR_VALUES = ['ANY', 'ALL', 'NONE'];
/**
* @var string|null The operator to use
*/
protected $operator;
/**
* @var string The value to compare to
*/
protected $value;
public function __construct(string $property, string $identifier = null, $value = null, string $operator = '')
{
parent::__construct($property, $identifier);
$this->value = $value;
$this->operator = $operator;
}
/**
* @return string
*/
public function getOperator(): ?string
{
return $this->operator;
}
/**
* @param string $operator
*/
public function setOperator(?string $operator): self
{
$this->operator = $operator;
return $this;
}
/**
* @return string
*/
public function getValue(): string
{
return $this->value;
}
/**
* @param string $value
*/
public function setValue(string $value): self
{
$this->value = $value;
return $this;
}
public function isEnabled(): bool
{
return $this->value !== null
&& !empty($this->operator);
}
/**
* Returns a list of tags based on the comma separated tags list
* @return string[]
*/
public function getTags(): array
{
return explode(',', trim($this->value, ','));
}
/**
* Builds an expression to query for a single tag
* @param QueryBuilder $queryBuilder
* @param string $tag
* @return Expr\Orx
*/
protected function getExpressionForTag(QueryBuilder $queryBuilder, string $tag): Expr\Orx
{
$tag_identifier_prefix = uniqid($this->identifier . '_', false);
$expr = $queryBuilder->expr();
$tmp = $expr->orX(
$expr->like($this->property, ':' . $tag_identifier_prefix . '_1'),
$expr->like($this->property, ':' . $tag_identifier_prefix . '_2'),
$expr->like($this->property, ':' . $tag_identifier_prefix . '_3'),
$expr->eq($this->property, ':' . $tag_identifier_prefix . '_4'),
);
//Set the parameters for the LIKE expression, in each variation of the tag (so with a comma, at the end, at the beginning, and on both ends, and equaling the tag)
$queryBuilder->setParameter($tag_identifier_prefix . '_1', '%,' . $tag . ',%');
$queryBuilder->setParameter($tag_identifier_prefix . '_2', '%,' . $tag);
$queryBuilder->setParameter($tag_identifier_prefix . '_3', $tag . ',%');
$queryBuilder->setParameter($tag_identifier_prefix . '_4', $tag);
return $tmp;
}
public function apply(QueryBuilder $queryBuilder): void
{
if(!$this->isEnabled()) {
return;
}
if(!in_array($this->operator, self::ALLOWED_OPERATOR_VALUES, true)) {
throw new \RuntimeException('Invalid operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES));
}
$tagsExpressions = [];
foreach ($this->getTags() as $tag) {
$tagsExpressions[] = $this->getExpressionForTag($queryBuilder, $tag);
}
if ($this->operator === 'ANY') {
$queryBuilder->andWhere($queryBuilder->expr()->orX(...$tagsExpressions));
return;
}
if ($this->operator === 'ALL') {
$queryBuilder->andWhere($queryBuilder->expr()->andX(...$tagsExpressions));
return;
}
if ($this->operator === 'NONE') {
$queryBuilder->andWhere($queryBuilder->expr()->not($queryBuilder->expr()->orX(...$tagsExpressions)));
return;
}
}
}

View file

@ -6,6 +6,7 @@ use App\DataTables\Filters\Constraints\BooleanConstraint;
use App\DataTables\Filters\Constraints\DateTimeConstraint;
use App\DataTables\Filters\Constraints\EntityConstraint;
use App\DataTables\Filters\Constraints\NumberConstraint;
use App\DataTables\Filters\Constraints\Part\TagsConstraint;
use App\DataTables\Filters\Constraints\TextConstraint;
use App\Entity\Parts\Category;
use App\Entity\Parts\Footprint;
@ -33,6 +34,9 @@ class PartFilter implements FilterInterface
/** @var TextConstraint */
protected $comment;
/** @var TagsConstraint */
protected $tags;
/** @var NumberConstraint */
protected $minAmount;
@ -82,6 +86,7 @@ class PartFilter implements FilterInterface
$this->comment = new TextConstraint('part.comment');
$this->category = new EntityConstraint($nodesListBuilder, Category::class, 'part.category');
$this->footprint = new EntityConstraint($nodesListBuilder, Footprint::class, 'part.footprint');
$this->tags = new TagsConstraint('part.tags');
$this->favorite = new BooleanConstraint('part.favorite');
$this->needsReview = new BooleanConstraint('part.needs_review');
@ -239,7 +244,11 @@ class PartFilter implements FilterInterface
return $this->manufacturer_product_number;
}
/**
* @return TagsConstraint
*/
public function getTags(): TagsConstraint
{
return $this->tags;
}
}

View file

@ -0,0 +1,56 @@
<?php
namespace App\Form\Filters\Constraints;
use App\DataTables\Filters\Constraints\Part\TagsConstraint;
use Symfony\Component\Form\AbstractType;
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
use Symfony\Component\Form\Extension\Core\Type\SearchType;
use Symfony\Component\Form\FormBuilderInterface;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Routing\Generator\UrlGeneratorInterface;
class TagsConstraintType extends AbstractType
{
protected $urlGenerator;
public function __construct(UrlGeneratorInterface $urlGenerator)
{
$this->urlGenerator = $urlGenerator;
}
public function configureOptions(OptionsResolver $resolver): void
{
$resolver->setDefaults([
'compound' => true,
'data_class' => TagsConstraint::class,
]);
}
public function buildForm(FormBuilderInterface $builder, array $options): void
{
$choices = [
'' => '',
'filter.tags_constraint.operator.ANY' => 'ANY',
'filter.tags_constraint.operator.ALL' => 'ALL',
'filter.tags_constraint.operator.NONE' => 'NONE'
];
$builder->add('value', SearchType::class, [
'attr' => [
'class' => 'tagsinput',
'data-controller' => 'elements--tagsinput',
'data-autocomplete' => $this->urlGenerator->generate('typeahead_tags', ['query' => '__QUERY__']),
],
'required' => false,
'empty_data' => '',
]);
$builder->add('operator', ChoiceType::class, [
'label' => 'filter.text_constraint.operator',
'choices' => $choices,
'required' => false,
]);
}
}

View file

@ -12,6 +12,7 @@ use App\Form\Filters\Constraints\BooleanConstraintType;
use App\Form\Filters\Constraints\DateTimeConstraintType;
use App\Form\Filters\Constraints\NumberConstraintType;
use App\Form\Filters\Constraints\StructuralEntityConstraintType;
use App\Form\Filters\Constraints\TagsConstraintType;
use App\Form\Filters\Constraints\TextConstraintType;
use Svg\Tag\Text;
use Symfony\Component\Form\AbstractType;
@ -56,6 +57,10 @@ class PartFilterType extends AbstractType
'entity_class' => Footprint::class
]);
$builder->add('tags', TagsConstraintType::class, [
'label' => 'part.edit.tags'
]);
$builder->add('comment', TextConstraintType::class, [
'label' => 'part.edit.comment'
]);

View file

@ -18,7 +18,7 @@
<div class="input-group">
{{ form_widget(form.operator, {"attr": {"class": "form-select"}}) }}
{{ form_widget(form.value) }}
{% if form.vars["text_suffix"] %}
{% if form.vars["text_suffix"] is defined and form.vars["text_suffix"] %}
<span class="input-group-text">{{ form.vars["text_suffix"] }}</span>
{% endif %}
</div>
@ -30,4 +30,8 @@
{% block date_time_constraint_widget %}
{{ block('number_constraint_widget') }}
{% endblock %}
{% block tags_constraint_widget %}
{{ block('text_constraint_widget') }}
{% endblock %}

View file

@ -27,6 +27,7 @@
{{ form_row(filterForm.description) }}
{{ form_row(filterForm.category) }}
{{ form_row(filterForm.footprint) }}
{{ form_row(filterForm.tags) }}
{{ form_row(filterForm.comment) }}
</div>

View file

@ -9441,5 +9441,23 @@ Element 3</target>
<target>Database ID</target>
</segment>
</unit>
<unit id="RphtSCZ" name="filter.tags_constraint.operator.ANY">
<segment>
<source>filter.tags_constraint.operator.ANY</source>
<target>Any of the tags</target>
</segment>
</unit>
<unit id="So3q9VW" name="filter.tags_constraint.operator.ALL">
<segment>
<source>filter.tags_constraint.operator.ALL</source>
<target>All of the tags</target>
</segment>
</unit>
<unit id="mqkIc_4" name="filter.tags_constraint.operator.NONE">
<segment>
<source>filter.tags_constraint.operator.NONE</source>
<target>None of the tags</target>
</segment>
</unit>
</file>
</xliff>