diff --git a/src/DataTables/Filters/Constraints/Part/TagsConstraint.php b/src/DataTables/Filters/Constraints/Part/TagsConstraint.php new file mode 100644 index 00000000..ecfb6274 --- /dev/null +++ b/src/DataTables/Filters/Constraints/Part/TagsConstraint.php @@ -0,0 +1,137 @@ +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; + } + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/PartFilter.php b/src/DataTables/Filters/PartFilter.php index 11725f6c..9b46e4e8 100644 --- a/src/DataTables/Filters/PartFilter.php +++ b/src/DataTables/Filters/PartFilter.php @@ -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; + } } diff --git a/src/Form/Filters/Constraints/TagsConstraintType.php b/src/Form/Filters/Constraints/TagsConstraintType.php new file mode 100644 index 00000000..c5c9d043 --- /dev/null +++ b/src/Form/Filters/Constraints/TagsConstraintType.php @@ -0,0 +1,56 @@ +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, + ]); + } +} \ No newline at end of file diff --git a/src/Form/Filters/PartFilterType.php b/src/Form/Filters/PartFilterType.php index b5e4186a..f1ed8d66 100644 --- a/src/Form/Filters/PartFilterType.php +++ b/src/Form/Filters/PartFilterType.php @@ -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' ]); diff --git a/templates/Form/FilterTypesLayout.html.twig b/templates/Form/FilterTypesLayout.html.twig index 776700b5..1656c6c0 100644 --- a/templates/Form/FilterTypesLayout.html.twig +++ b/templates/Form/FilterTypesLayout.html.twig @@ -18,7 +18,7 @@
{{ 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"] %} {{ form.vars["text_suffix"] }} {% endif %}
@@ -30,4 +30,8 @@ {% block date_time_constraint_widget %} {{ block('number_constraint_widget') }} +{% endblock %} + +{% block tags_constraint_widget %} + {{ block('text_constraint_widget') }} {% endblock %} \ No newline at end of file diff --git a/templates/Parts/lists/_filter.html.twig b/templates/Parts/lists/_filter.html.twig index 833c61bb..6a999fd9 100644 --- a/templates/Parts/lists/_filter.html.twig +++ b/templates/Parts/lists/_filter.html.twig @@ -27,6 +27,7 @@ {{ form_row(filterForm.description) }} {{ form_row(filterForm.category) }} {{ form_row(filterForm.footprint) }} + {{ form_row(filterForm.tags) }} {{ form_row(filterForm.comment) }} diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 012f926d..db335770 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -9441,5 +9441,23 @@ Element 3 Database ID + + + filter.tags_constraint.operator.ANY + Any of the tags + + + + + filter.tags_constraint.operator.ALL + All of the tags + + + + + filter.tags_constraint.operator.NONE + None of the tags + +