mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 01:25:55 +02:00
Added an filter constraint based on part tags.
This commit is contained in:
parent
4d3ff7d7b5
commit
4ba58cc621
7 changed files with 234 additions and 4 deletions
137
src/DataTables/Filters/Constraints/Part/TagsConstraint.php
Normal file
137
src/DataTables/Filters/Constraints/Part/TagsConstraint.php
Normal 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;
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
56
src/Form/Filters/Constraints/TagsConstraintType.php
Normal file
56
src/Form/Filters/Constraints/TagsConstraintType.php
Normal 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,
|
||||
]);
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
]);
|
||||
|
|
|
@ -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 %}
|
|
@ -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>
|
||||
|
||||
|
|
|
@ -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>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue