mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 09:35:49 +02:00
Added filter constraint for manufacturing status.
This commit is contained in:
parent
7b3538a2c7
commit
ec5e956e31
11 changed files with 342 additions and 1 deletions
15
assets/controllers/elements/select_multiple_controller.js
Normal file
15
assets/controllers/elements/select_multiple_controller.js
Normal file
|
@ -0,0 +1,15 @@
|
||||||
|
import {Controller} from "@hotwired/stimulus";
|
||||||
|
import TomSelect from "tom-select";
|
||||||
|
|
||||||
|
export default class extends Controller {
|
||||||
|
_tomSelect;
|
||||||
|
|
||||||
|
connect() {
|
||||||
|
this._tomSelect = new TomSelect(this.element, {
|
||||||
|
maxItems: 1000,
|
||||||
|
allowEmptyOption: true,
|
||||||
|
plugins: ['remove_button'],
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
84
src/DataTables/Filters/Constraints/ChoiceConstraint.php
Normal file
84
src/DataTables/Filters/Constraints/ChoiceConstraint.php
Normal file
|
@ -0,0 +1,84 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\DataTables\Filters\Constraints;
|
||||||
|
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
|
||||||
|
class ChoiceConstraint extends AbstractConstraint
|
||||||
|
{
|
||||||
|
public const ALLOWED_OPERATOR_VALUES = ['ANY', 'NONE'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string[] The values to compare to
|
||||||
|
*/
|
||||||
|
protected $value;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string The operator to use
|
||||||
|
*/
|
||||||
|
protected $operator;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string[]
|
||||||
|
*/
|
||||||
|
public function getValue(): array
|
||||||
|
{
|
||||||
|
return $this->value;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string[] $value
|
||||||
|
* @return ChoiceConstraint
|
||||||
|
*/
|
||||||
|
public function setValue(array $value): ChoiceConstraint
|
||||||
|
{
|
||||||
|
$this->value = $value;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return string
|
||||||
|
*/
|
||||||
|
public function getOperator(): string
|
||||||
|
{
|
||||||
|
return $this->operator;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param string $operator
|
||||||
|
* @return ChoiceConstraint
|
||||||
|
*/
|
||||||
|
public function setOperator(string $operator): ChoiceConstraint
|
||||||
|
{
|
||||||
|
$this->operator = $operator;
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public function isEnabled(): bool
|
||||||
|
{
|
||||||
|
return !empty($this->operator);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function apply(QueryBuilder $queryBuilder): void
|
||||||
|
{
|
||||||
|
//If no value is provided then we do not apply a filter
|
||||||
|
if (!$this->isEnabled()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//Ensure we have an valid operator
|
||||||
|
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));
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->operator === 'ANY') {
|
||||||
|
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'IN', $this->value);
|
||||||
|
} elseif ($this->operator === 'NONE') {
|
||||||
|
$this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'NOT IN', $this->value);
|
||||||
|
} else {
|
||||||
|
throw new \RuntimeException('Unknown operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -47,7 +47,7 @@ trait FilterTrait
|
||||||
*/
|
*/
|
||||||
protected function addSimpleAndConstraint(QueryBuilder $queryBuilder, string $property, string $parameterIdentifier, string $comparison_operator, $value): void
|
protected function addSimpleAndConstraint(QueryBuilder $queryBuilder, string $property, string $parameterIdentifier, string $comparison_operator, $value): void
|
||||||
{
|
{
|
||||||
if ($comparison_operator === 'IN') {
|
if ($comparison_operator === 'IN' || $comparison_operator === 'NOT IN') {
|
||||||
$expression = sprintf("%s %s (:%s)", $property, $comparison_operator, $parameterIdentifier);
|
$expression = sprintf("%s %s (:%s)", $property, $comparison_operator, $parameterIdentifier);
|
||||||
} else {
|
} else {
|
||||||
$expression = sprintf("%s %s :%s", $property, $comparison_operator, $parameterIdentifier);
|
$expression = sprintf("%s %s :%s", $property, $comparison_operator, $parameterIdentifier);
|
||||||
|
|
|
@ -3,6 +3,7 @@
|
||||||
namespace App\DataTables\Filters;
|
namespace App\DataTables\Filters;
|
||||||
|
|
||||||
use App\DataTables\Filters\Constraints\BooleanConstraint;
|
use App\DataTables\Filters\Constraints\BooleanConstraint;
|
||||||
|
use App\DataTables\Filters\Constraints\ChoiceConstraint;
|
||||||
use App\DataTables\Filters\Constraints\DateTimeConstraint;
|
use App\DataTables\Filters\Constraints\DateTimeConstraint;
|
||||||
use App\DataTables\Filters\Constraints\EntityConstraint;
|
use App\DataTables\Filters\Constraints\EntityConstraint;
|
||||||
use App\DataTables\Filters\Constraints\IntConstraint;
|
use App\DataTables\Filters\Constraints\IntConstraint;
|
||||||
|
@ -67,6 +68,9 @@ class PartFilter implements FilterInterface
|
||||||
/** @var EntityConstraint */
|
/** @var EntityConstraint */
|
||||||
protected $manufacturer;
|
protected $manufacturer;
|
||||||
|
|
||||||
|
/** @var ChoiceConstraint */
|
||||||
|
protected $manufacturing_status;
|
||||||
|
|
||||||
/** @var EntityConstraint */
|
/** @var EntityConstraint */
|
||||||
protected $supplier;
|
protected $supplier;
|
||||||
|
|
||||||
|
@ -133,6 +137,7 @@ class PartFilter implements FilterInterface
|
||||||
$this->manufacturer = new EntityConstraint($nodesListBuilder, Manufacturer::class, 'part.manufacturer');
|
$this->manufacturer = new EntityConstraint($nodesListBuilder, Manufacturer::class, 'part.manufacturer');
|
||||||
$this->manufacturer_product_number = new TextConstraint('part.manufacturer_product_number');
|
$this->manufacturer_product_number = new TextConstraint('part.manufacturer_product_number');
|
||||||
$this->manufacturer_product_url = new TextConstraint('part.manufacturer_product_url');
|
$this->manufacturer_product_url = new TextConstraint('part.manufacturer_product_url');
|
||||||
|
$this->manufacturing_status = new ChoiceConstraint('part.manufacturing_status');
|
||||||
|
|
||||||
$this->storelocation = new EntityConstraint($nodesListBuilder, Storelocation::class, 'partLots.storage_location');
|
$this->storelocation = new EntityConstraint($nodesListBuilder, Storelocation::class, 'partLots.storage_location');
|
||||||
|
|
||||||
|
@ -350,6 +355,11 @@ class PartFilter implements FilterInterface
|
||||||
return $this->attachmentName;
|
return $this->attachmentName;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public function getManufacturingStatus(): ChoiceConstraint
|
||||||
|
{
|
||||||
|
return $this->manufacturing_status;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
48
src/Form/Filters/Constraints/ChoiceConstraintType.php
Normal file
48
src/Form/Filters/Constraints/ChoiceConstraintType.php
Normal file
|
@ -0,0 +1,48 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Form\Filters\Constraints;
|
||||||
|
|
||||||
|
use App\DataTables\Filters\Constraints\ChoiceConstraint;
|
||||||
|
use Symfony\Component\Form\AbstractType;
|
||||||
|
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||||
|
use Symfony\Component\Form\FormBuilderInterface;
|
||||||
|
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||||
|
|
||||||
|
class ChoiceConstraintType extends AbstractType
|
||||||
|
{
|
||||||
|
public function configureOptions(OptionsResolver $resolver): void
|
||||||
|
{
|
||||||
|
$resolver->setRequired('choices');
|
||||||
|
$resolver->setAllowedTypes('choices', 'array');
|
||||||
|
|
||||||
|
$resolver->setDefaults([
|
||||||
|
'compound' => true,
|
||||||
|
'data_class' => ChoiceConstraint::class,
|
||||||
|
]);
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public function buildForm(FormBuilderInterface $builder, array $options): void
|
||||||
|
{
|
||||||
|
$choices = [
|
||||||
|
'' => '',
|
||||||
|
'filter.choice_constraint.operator.ANY' => 'ANY',
|
||||||
|
'filter.choice_constraint.operator.NONE' => 'NONE',
|
||||||
|
];
|
||||||
|
|
||||||
|
$builder->add('operator', ChoiceType::class, [
|
||||||
|
'choices' => $choices,
|
||||||
|
'required' => false,
|
||||||
|
]);
|
||||||
|
|
||||||
|
$builder->add('value', ChoiceType::class, [
|
||||||
|
'choices' => $options['choices'],
|
||||||
|
'required' => false,
|
||||||
|
'multiple' => true,
|
||||||
|
'attr' => [
|
||||||
|
'data-controller' => 'elements--select-multiple',
|
||||||
|
]
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ use App\Entity\Parts\Manufacturer;
|
||||||
use App\Entity\Parts\MeasurementUnit;
|
use App\Entity\Parts\MeasurementUnit;
|
||||||
use App\Entity\Parts\Storelocation;
|
use App\Entity\Parts\Storelocation;
|
||||||
use App\Form\Filters\Constraints\BooleanConstraintType;
|
use App\Form\Filters\Constraints\BooleanConstraintType;
|
||||||
|
use App\Form\Filters\Constraints\ChoiceConstraintType;
|
||||||
use App\Form\Filters\Constraints\DateTimeConstraintType;
|
use App\Form\Filters\Constraints\DateTimeConstraintType;
|
||||||
use App\Form\Filters\Constraints\NumberConstraintType;
|
use App\Form\Filters\Constraints\NumberConstraintType;
|
||||||
use App\Form\Filters\Constraints\StructuralEntityConstraintType;
|
use App\Form\Filters\Constraints\StructuralEntityConstraintType;
|
||||||
|
@ -123,6 +124,20 @@ class PartFilterType extends AbstractType
|
||||||
'label' => 'part.edit.mpn'
|
'label' => 'part.edit.mpn'
|
||||||
]);
|
]);
|
||||||
|
|
||||||
|
$status_choices = [
|
||||||
|
'm_status.unknown' => '',
|
||||||
|
'm_status.announced' => 'announced',
|
||||||
|
'm_status.active' => 'active',
|
||||||
|
'm_status.nrfnd' => 'nrfnd',
|
||||||
|
'm_status.eol' => 'eol',
|
||||||
|
'm_status.discontinued' => 'discontinued',
|
||||||
|
];
|
||||||
|
|
||||||
|
$builder->add('manufacturing_status', ChoiceConstraintType::class, [
|
||||||
|
'label' => 'part.edit.manufacturing_status',
|
||||||
|
'choices' => $status_choices,
|
||||||
|
]);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Purchasee informations
|
* Purchasee informations
|
||||||
*/
|
*/
|
||||||
|
|
|
@ -34,4 +34,8 @@
|
||||||
|
|
||||||
{% block tags_constraint_widget %}
|
{% block tags_constraint_widget %}
|
||||||
{{ block('text_constraint_widget') }}
|
{{ block('text_constraint_widget') }}
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block choice_constraint_widget %}
|
||||||
|
{{ block('text_constraint_widget') }}
|
||||||
{% endblock %}
|
{% endblock %}
|
|
@ -36,6 +36,7 @@
|
||||||
|
|
||||||
<div class="tab-pane pt-3" id="filter-manufacturer" role="tabpanel" aria-labelledby="filter-manufacturer-tab" tabindex="0">
|
<div class="tab-pane pt-3" id="filter-manufacturer" role="tabpanel" aria-labelledby="filter-manufacturer-tab" tabindex="0">
|
||||||
{{ form_row(filterForm.manufacturer) }}
|
{{ form_row(filterForm.manufacturer) }}
|
||||||
|
{{ form_row(filterForm.manufacturing_status) }}
|
||||||
{{ form_row(filterForm.manufacturer_product_number) }}
|
{{ form_row(filterForm.manufacturer_product_number) }}
|
||||||
{{ form_row(filterForm.manufacturer_product_url) }}
|
{{ form_row(filterForm.manufacturer_product_url) }}
|
||||||
</div>
|
</div>
|
||||||
|
|
112
tests/DataTables/Filters/CompoundFilterTraitTest.php
Normal file
112
tests/DataTables/Filters/CompoundFilterTraitTest.php
Normal file
|
@ -0,0 +1,112 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\DataTables\Filters;
|
||||||
|
|
||||||
|
use App\DataTables\Filters\CompoundFilterTrait;
|
||||||
|
use App\DataTables\Filters\FilterInterface;
|
||||||
|
use Doctrine\ORM\QueryBuilder;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class CompoundFilterTraitTest extends TestCase
|
||||||
|
{
|
||||||
|
|
||||||
|
public function testFindAllChildFiltersEmpty(): void
|
||||||
|
{
|
||||||
|
$filter = new class {
|
||||||
|
use CompoundFilterTrait;
|
||||||
|
|
||||||
|
public function _findAllChildFilters()
|
||||||
|
{
|
||||||
|
return $this->findAllChildFilters();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$result = $filter->_findAllChildFilters();
|
||||||
|
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertEmpty($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testFindAllChildFilters(): void
|
||||||
|
{
|
||||||
|
$f1 = $this->createMock(FilterInterface::class);
|
||||||
|
$f2 = $this->createMock(FilterInterface::class);
|
||||||
|
$f3 = $this->createMock(FilterInterface::class);
|
||||||
|
|
||||||
|
$filter = new class($f1, $f2, $f3, null) {
|
||||||
|
use CompoundFilterTrait;
|
||||||
|
|
||||||
|
protected $filter1;
|
||||||
|
private $filter2;
|
||||||
|
public $filter3;
|
||||||
|
protected $filter4;
|
||||||
|
|
||||||
|
public function __construct($f1, $f2, $f3, $f4) {
|
||||||
|
$this->filter1 = $f1;
|
||||||
|
$this->filter2 = $f2;
|
||||||
|
$this->filter3 = $f3;
|
||||||
|
$this->filter4 = $f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function _findAllChildFilters()
|
||||||
|
{
|
||||||
|
return $this->findAllChildFilters();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$result = $filter->_findAllChildFilters();
|
||||||
|
|
||||||
|
$this->assertIsArray($result);
|
||||||
|
$this->assertContainsOnlyInstancesOf(FilterInterface::class, $result);
|
||||||
|
$this->assertSame([
|
||||||
|
'filter1' => $f1,
|
||||||
|
'filter2' => $f2,
|
||||||
|
'filter3' => $f3
|
||||||
|
], $result);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function testApplyAllChildFilters(): void
|
||||||
|
{
|
||||||
|
$f1 = $this->createMock(FilterInterface::class);
|
||||||
|
$f2 = $this->createMock(FilterInterface::class);
|
||||||
|
$f3 = $this->createMock(FilterInterface::class);
|
||||||
|
|
||||||
|
$f1->expects($this->once())
|
||||||
|
->method('apply')
|
||||||
|
->with($this->isInstanceOf(QueryBuilder::class));
|
||||||
|
|
||||||
|
$f2->expects($this->once())
|
||||||
|
->method('apply')
|
||||||
|
->with($this->isInstanceOf(QueryBuilder::class));
|
||||||
|
|
||||||
|
$f3->expects($this->once())
|
||||||
|
->method('apply')
|
||||||
|
->with($this->isInstanceOf(QueryBuilder::class));
|
||||||
|
|
||||||
|
$filter = new class($f1, $f2, $f3, null) {
|
||||||
|
use CompoundFilterTrait;
|
||||||
|
|
||||||
|
protected $filter1;
|
||||||
|
private $filter2;
|
||||||
|
public $filter3;
|
||||||
|
protected $filter4;
|
||||||
|
|
||||||
|
public function __construct($f1, $f2, $f3, $f4) {
|
||||||
|
$this->filter1 = $f1;
|
||||||
|
$this->filter2 = $f2;
|
||||||
|
$this->filter3 = $f3;
|
||||||
|
$this->filter4 = $f4;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function _applyAllChildFilters(QueryBuilder $queryBuilder): void
|
||||||
|
{
|
||||||
|
$this->applyAllChildFilters($queryBuilder);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$qb = $this->createMock(QueryBuilder::class);
|
||||||
|
$filter->_applyAllChildFilters($qb);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
40
tests/DataTables/Filters/Constraints/FilterTraitTest.php
Normal file
40
tests/DataTables/Filters/Constraints/FilterTraitTest.php
Normal file
|
@ -0,0 +1,40 @@
|
||||||
|
<?php
|
||||||
|
|
||||||
|
namespace App\Tests\DataTables\Filters\Constraints;
|
||||||
|
|
||||||
|
use App\DataTables\Filters\Constraints\FilterTrait;
|
||||||
|
use App\Entity\Parts\MeasurementUnit;
|
||||||
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
|
class FilterTraitTest extends TestCase
|
||||||
|
{
|
||||||
|
use FilterTrait;
|
||||||
|
|
||||||
|
public function testUseHaving(): void
|
||||||
|
{
|
||||||
|
$this->assertFalse($this->useHaving);
|
||||||
|
|
||||||
|
$this->useHaving();
|
||||||
|
$this->assertTrue($this->useHaving);
|
||||||
|
|
||||||
|
$this->useHaving(false);
|
||||||
|
$this->assertFalse($this->useHaving);
|
||||||
|
}
|
||||||
|
|
||||||
|
public function isAggregateFunctionStringDataProvider(): iterable
|
||||||
|
{
|
||||||
|
yield [false, 'parts.test'];
|
||||||
|
yield [false, 'attachments.test'];
|
||||||
|
yield [true, 'COUNT(attachments)'];
|
||||||
|
yield [true, 'MAX(attachments.value)'];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @dataProvider isAggregateFunctionStringDataProvider
|
||||||
|
*/
|
||||||
|
public function testIsAggregateFunctionString(bool $expected, string $input): void
|
||||||
|
{
|
||||||
|
$this->assertEquals($expected, $this->isAggregateFunctionString($input));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
|
@ -9501,5 +9501,17 @@ Element 3</target>
|
||||||
<target>Attachment name</target>
|
<target>Attachment name</target>
|
||||||
</segment>
|
</segment>
|
||||||
</unit>
|
</unit>
|
||||||
|
<unit id=".gg9fsx" name="filter.choice_constraint.operator.ANY">
|
||||||
|
<segment>
|
||||||
|
<source>filter.choice_constraint.operator.ANY</source>
|
||||||
|
<target>Any of</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
|
<unit id="dbzWAHM" name="filter.choice_constraint.operator.NONE">
|
||||||
|
<segment>
|
||||||
|
<source>filter.choice_constraint.operator.NONE</source>
|
||||||
|
<target>None of</target>
|
||||||
|
</segment>
|
||||||
|
</unit>
|
||||||
</file>
|
</file>
|
||||||
</xliff>
|
</xliff>
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue