mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-20 17:15:51 +02:00
Added filter possibility to attachment list
This commit is contained in:
parent
bee057bc4b
commit
017b0f717e
8 changed files with 440 additions and 2 deletions
|
@ -43,9 +43,15 @@ declare(strict_types=1);
|
|||
namespace App\Controller;
|
||||
|
||||
use App\DataTables\AttachmentDataTable;
|
||||
use App\DataTables\Filters\AttachmentFilter;
|
||||
use App\DataTables\Filters\PartFilter;
|
||||
use App\DataTables\PartsDataTable;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Entity\Attachments\PartAttachment;
|
||||
use App\Form\Filters\AttachmentFilterType;
|
||||
use App\Form\Filters\PartFilterType;
|
||||
use App\Services\Attachments\AttachmentManager;
|
||||
use App\Services\Trees\NodesListBuilder;
|
||||
use Omines\DataTablesBundle\DataTableFactory;
|
||||
use RuntimeException;
|
||||
use Symfony\Bundle\FrameworkBundle\Controller\AbstractController;
|
||||
|
@ -123,11 +129,19 @@ class AttachmentFileController extends AbstractController
|
|||
*
|
||||
* @return JsonResponse|Response
|
||||
*/
|
||||
public function attachmentsTable(DataTableFactory $dataTable, Request $request)
|
||||
public function attachmentsTable(Request $request, DataTableFactory $dataTableFactory, NodesListBuilder $nodesListBuilder)
|
||||
{
|
||||
$this->denyAccessUnlessGranted('read', new PartAttachment());
|
||||
|
||||
$table = $dataTable->createFromType(AttachmentDataTable::class)
|
||||
$formRequest = clone $request;
|
||||
$formRequest->setMethod('GET');
|
||||
$filter = new AttachmentFilter($nodesListBuilder);
|
||||
|
||||
$filterForm = $this->createForm(AttachmentFilterType::class, $filter, ['method' => 'GET']);
|
||||
|
||||
$filterForm->handleRequest($formRequest);
|
||||
|
||||
$table = $dataTableFactory->createFromType(AttachmentDataTable::class, ['filter' => $filter])
|
||||
->handleRequest($request);
|
||||
|
||||
if ($table->isCallback()) {
|
||||
|
@ -136,6 +150,7 @@ class AttachmentFileController extends AbstractController
|
|||
|
||||
return $this->render('attachment_list.html.twig', [
|
||||
'datatable' => $table,
|
||||
'filterForm' => $filterForm->createView(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -44,12 +44,14 @@ namespace App\DataTables;
|
|||
|
||||
use App\DataTables\Column\LocaleDateTimeColumn;
|
||||
use App\DataTables\Column\PrettyBoolColumn;
|
||||
use App\DataTables\Filters\AttachmentFilter;
|
||||
use App\Entity\Attachments\Attachment;
|
||||
use App\Services\Attachments\AttachmentManager;
|
||||
use App\Services\Attachments\AttachmentURLGenerator;
|
||||
use App\Services\ElementTypeNameGenerator;
|
||||
use App\Services\EntityURLGenerator;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
use Omines\DataTablesBundle\Adapter\Doctrine\ORM\SearchCriteriaProvider;
|
||||
use Omines\DataTablesBundle\Adapter\Doctrine\ORMAdapter;
|
||||
use Omines\DataTablesBundle\Column\TextColumn;
|
||||
use Omines\DataTablesBundle\DataTable;
|
||||
|
@ -213,6 +215,12 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
|||
'query' => function (QueryBuilder $builder): void {
|
||||
$this->getQuery($builder);
|
||||
},
|
||||
'criteria' => [
|
||||
function (QueryBuilder $builder) use ($options): void {
|
||||
$this->buildCriteria($builder, $options);
|
||||
},
|
||||
new SearchCriteriaProvider(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
|
@ -225,4 +233,18 @@ final class AttachmentDataTable implements DataTableTypeInterface
|
|||
->leftJoin('attachment.attachment_type', 'attachment_type');
|
||||
//->leftJoin('attachment.element', 'element');
|
||||
}
|
||||
|
||||
private function buildCriteria(QueryBuilder $builder, array $options): void
|
||||
{
|
||||
//We do the most stuff here in the filter class
|
||||
if (isset($options['filter'])) {
|
||||
if(!$options['filter'] instanceof AttachmentFilter) {
|
||||
throw new \Exception('filter must be an instance of AttachmentFilter!');
|
||||
}
|
||||
|
||||
$filter = $options['filter'];
|
||||
$filter->apply($builder);
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
|
|
120
src/DataTables/Filters/AttachmentFilter.php
Normal file
120
src/DataTables/Filters/AttachmentFilter.php
Normal file
|
@ -0,0 +1,120 @@
|
|||
<?php
|
||||
|
||||
namespace App\DataTables\Filters;
|
||||
|
||||
use App\DataTables\Filters\Constraints\BooleanConstraint;
|
||||
use App\DataTables\Filters\Constraints\DateTimeConstraint;
|
||||
use App\DataTables\Filters\Constraints\EntityConstraint;
|
||||
use App\DataTables\Filters\Constraints\InstanceOfConstraint;
|
||||
use App\DataTables\Filters\Constraints\NumberConstraint;
|
||||
use App\DataTables\Filters\Constraints\TextConstraint;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
use App\Services\Trees\NodesListBuilder;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
class AttachmentFilter implements FilterInterface
|
||||
{
|
||||
use CompoundFilterTrait;
|
||||
|
||||
/** @var NumberConstraint */
|
||||
protected $dbId;
|
||||
|
||||
/** @var InstanceOfConstraint */
|
||||
protected $targetType;
|
||||
|
||||
/** @var TextConstraint */
|
||||
protected $name;
|
||||
|
||||
/** @var EntityConstraint */
|
||||
protected $attachmentType;
|
||||
|
||||
/** @var BooleanConstraint */
|
||||
protected $showInTable;
|
||||
|
||||
/** @var DateTimeConstraint */
|
||||
protected $lastModified;
|
||||
|
||||
/** @var DateTimeConstraint */
|
||||
protected $addedDate;
|
||||
|
||||
|
||||
public function __construct(NodesListBuilder $nodesListBuilder)
|
||||
{
|
||||
$this->dbId = new NumberConstraint('attachment.id');
|
||||
$this->name = new TextConstraint('attachment.name');
|
||||
$this->targetType = new InstanceOfConstraint('attachment');
|
||||
$this->attachmentType = new EntityConstraint($nodesListBuilder, AttachmentType::class, 'attachment.attachment_type');
|
||||
$this->lastModified = new DateTimeConstraint('attachment.lastModified');
|
||||
$this->addedDate = new DateTimeConstraint('attachment.addedDate');
|
||||
$this->showInTable = new BooleanConstraint('attachment.show_in_table');
|
||||
}
|
||||
|
||||
public function apply(QueryBuilder $queryBuilder): void
|
||||
{
|
||||
$this->applyAllChildFilters($queryBuilder);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return NumberConstraint
|
||||
*/
|
||||
public function getDbId(): NumberConstraint
|
||||
{
|
||||
return $this->dbId;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TextConstraint
|
||||
*/
|
||||
public function getName(): TextConstraint
|
||||
{
|
||||
return $this->name;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DateTimeConstraint
|
||||
*/
|
||||
public function getLastModified(): DateTimeConstraint
|
||||
{
|
||||
return $this->lastModified;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return DateTimeConstraint
|
||||
*/
|
||||
public function getAddedDate(): DateTimeConstraint
|
||||
{
|
||||
return $this->addedDate;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return BooleanConstraint
|
||||
*/
|
||||
public function getShowInTable(): BooleanConstraint
|
||||
{
|
||||
return $this->showInTable;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @return EntityConstraint
|
||||
*/
|
||||
public function getAttachmentType(): EntityConstraint
|
||||
{
|
||||
return $this->attachmentType;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return InstanceOfConstraint
|
||||
*/
|
||||
public function getTargetType(): InstanceOfConstraint
|
||||
{
|
||||
return $this->targetType;
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
}
|
96
src/DataTables/Filters/Constraints/InstanceOfConstraint.php
Normal file
96
src/DataTables/Filters/Constraints/InstanceOfConstraint.php
Normal file
|
@ -0,0 +1,96 @@
|
|||
<?php
|
||||
|
||||
namespace App\DataTables\Filters\Constraints;
|
||||
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
/**
|
||||
* This constraint allows to filter by a given list of classes, that the given property should be an instance of
|
||||
*/
|
||||
class InstanceOfConstraint extends AbstractConstraint
|
||||
{
|
||||
public const ALLOWED_OPERATOR_VALUES = ['ANY', 'NONE'];
|
||||
|
||||
/**
|
||||
* @var string[] The values to compare to (fully qualified class names)
|
||||
*/
|
||||
protected $value;
|
||||
|
||||
/**
|
||||
* @var string The operator to use
|
||||
*/
|
||||
protected $operator;
|
||||
|
||||
/**
|
||||
* @return string[]
|
||||
*/
|
||||
public function getValue(): array
|
||||
{
|
||||
return $this->value;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string[] $value
|
||||
* @return $this
|
||||
*/
|
||||
public function setValue(array $value): self
|
||||
{
|
||||
$this->value = $value;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return string
|
||||
*/
|
||||
public function getOperator(): string
|
||||
{
|
||||
return $this->operator;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $operator
|
||||
* @return $this
|
||||
*/
|
||||
public function setOperator(string $operator): self
|
||||
{
|
||||
$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));
|
||||
}
|
||||
|
||||
$expressions = [];
|
||||
|
||||
if ($this->operator === 'ANY' || $this->operator === 'NONE') {
|
||||
foreach($this->value as $value) {
|
||||
//We cannnot use an paramater here, as this is the only way to pass the FCQN to the query (via binded params, we would need to use ClassMetaData). See: https://github.com/doctrine/orm/issues/4462
|
||||
$expressions[] = ($queryBuilder->expr()->isInstanceOf($this->property, $value));
|
||||
}
|
||||
|
||||
if($this->operator === 'ANY') {
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->orX(...$expressions));
|
||||
} else { //NONE
|
||||
$queryBuilder->andWhere($queryBuilder->expr()->not($queryBuilder->expr()->orX(...$expressions)));
|
||||
}
|
||||
} else {
|
||||
throw new \RuntimeException('Unknown operator '. $this->operator . ' provided. Valid operators are '. implode(', ', self::ALLOWED_OPERATOR_VALUES));
|
||||
}
|
||||
}
|
||||
}
|
101
src/Form/Filters/AttachmentFilterType.php
Normal file
101
src/Form/Filters/AttachmentFilterType.php
Normal file
|
@ -0,0 +1,101 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Filters;
|
||||
|
||||
use App\DataTables\Filters\AttachmentFilter;
|
||||
use App\Entity\Attachments\AttachmentType;
|
||||
use App\Entity\Attachments\AttachmentTypeAttachment;
|
||||
use App\Entity\Attachments\CategoryAttachment;
|
||||
use App\Entity\Attachments\CurrencyAttachment;
|
||||
use App\Entity\Attachments\DeviceAttachment;
|
||||
use App\Entity\Attachments\FootprintAttachment;
|
||||
use App\Entity\Attachments\GroupAttachment;
|
||||
use App\Entity\Attachments\LabelAttachment;
|
||||
use App\Entity\Attachments\PartAttachment;
|
||||
use App\Entity\Attachments\StorelocationAttachment;
|
||||
use App\Entity\Attachments\SupplierAttachment;
|
||||
use App\Entity\Attachments\UserAttachment;
|
||||
use App\Entity\Parts\Manufacturer;
|
||||
use App\Entity\Parts\MeasurementUnit;
|
||||
use App\Entity\Parts\Supplier;
|
||||
use App\Form\AdminPages\FootprintAdminForm;
|
||||
use App\Form\Filters\Constraints\BooleanConstraintType;
|
||||
use App\Form\Filters\Constraints\DateTimeConstraintType;
|
||||
use App\Form\Filters\Constraints\InstanceOfConstraintType;
|
||||
use App\Form\Filters\Constraints\NumberConstraintType;
|
||||
use App\Form\Filters\Constraints\StructuralEntityConstraintType;
|
||||
use App\Form\Filters\Constraints\TextConstraintType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ResetType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\SubmitType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class AttachmentFilterType extends AbstractType
|
||||
{
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->setDefaults([
|
||||
'compound' => true,
|
||||
'data_class' => AttachmentFilter::class,
|
||||
'csrf_protection' => false,
|
||||
]);
|
||||
}
|
||||
|
||||
public function buildForm(FormBuilderInterface $builder, array $options)
|
||||
{
|
||||
$builder->add('dbId', NumberConstraintType::class, [
|
||||
'label' => 'part.filter.dbId',
|
||||
'min' => 1,
|
||||
'step' => 1,
|
||||
]);
|
||||
|
||||
$builder->add('name', TextConstraintType::class, [
|
||||
'label' => 'attachment.edit.name',
|
||||
]);
|
||||
|
||||
$builder->add('targetType', InstanceOfConstraintType::class, [
|
||||
'label' => 'attachment.table.element_type',
|
||||
'choices' => [
|
||||
'part.label' => PartAttachment::class,
|
||||
'attachment_type.label' => AttachmentTypeAttachment::class,
|
||||
'category.label' => CategoryAttachment::class,
|
||||
'currency.label' => CurrencyAttachment::class,
|
||||
'device.label' => DeviceAttachment::class,
|
||||
'footprint.label' => FootprintAttachment::class,
|
||||
'group.label' => GroupAttachment::class,
|
||||
'label_profile.label' => LabelAttachment::class,
|
||||
'manufacturer.label' => Manufacturer::class,
|
||||
'measurement_unit.label' => MeasurementUnit::class,
|
||||
'storelocation.label' => StorelocationAttachment::class,
|
||||
'supplier.label' => SupplierAttachment::class,
|
||||
'user.label' => UserAttachment::class,
|
||||
]
|
||||
]);
|
||||
|
||||
$builder->add('attachmentType', StructuralEntityConstraintType::class, [
|
||||
'label' => 'attachment.attachment_type',
|
||||
'entity_class' => AttachmentType::class
|
||||
]);
|
||||
|
||||
$builder->add('showInTable', BooleanConstraintType::class, [
|
||||
'label' => 'attachment.edit.show_in_table'
|
||||
]);
|
||||
|
||||
$builder->add('lastModified', DateTimeConstraintType::class, [
|
||||
'label' => 'lastModified'
|
||||
]);
|
||||
|
||||
$builder->add('addedDate', DateTimeConstraintType::class, [
|
||||
'label' => 'createdAt'
|
||||
]);
|
||||
|
||||
$builder->add('submit', SubmitType::class, [
|
||||
'label' => 'filter.submit',
|
||||
]);
|
||||
|
||||
$builder->add('discard', ResetType::class, [
|
||||
'label' => 'filter.discard',
|
||||
]);
|
||||
}
|
||||
}
|
30
src/Form/Filters/Constraints/InstanceOfConstraintType.php
Normal file
30
src/Form/Filters/Constraints/InstanceOfConstraintType.php
Normal file
|
@ -0,0 +1,30 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Filters\Constraints;
|
||||
|
||||
use App\DataTables\Filters\Constraints\InstanceOfConstraint;
|
||||
use Doctrine\ORM\EntityManagerInterface;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\OptionsResolver\Options;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class InstanceOfConstraintType extends AbstractType
|
||||
{
|
||||
protected $em;
|
||||
|
||||
public function __construct(EntityManagerInterface $entityManager)
|
||||
{
|
||||
$this->em = $entityManager;
|
||||
}
|
||||
|
||||
public function configureOptions(OptionsResolver $resolver)
|
||||
{
|
||||
$resolver->setDefault('data_class', InstanceOfConstraint::class);
|
||||
}
|
||||
|
||||
public function getParent()
|
||||
{
|
||||
return ChoiceConstraintType::class;
|
||||
}
|
||||
}
|
|
@ -5,5 +5,53 @@
|
|||
{% block title %}{% trans %}attachment.list.title{% endtrans %}{% endblock %}
|
||||
|
||||
{% block content %}
|
||||
<div class="accordion mb-3" id="listAccordion">
|
||||
<div class="accordion-item">
|
||||
<div class="accordion-header">
|
||||
<button class="accordion-button collapsed py-2" data-bs-toggle="collapse" data-bs-target="#searchInfo" disabled>
|
||||
<i class="fa-solid fa-paperclip fa-fw"></i>
|
||||
{% trans %}attachment.list.title{% endtrans %}
|
||||
</button>
|
||||
</div>
|
||||
<div id="searchInfo" class="accordion-collapse collapse" data-bs-parent="#listAccordion">
|
||||
<div class="accordion-body">
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="accordion-item">
|
||||
<div class="accordion-header">
|
||||
<button class="accordion-button collapsed py-2" type="button" data-bs-toggle="collapse" data-bs-target="#filterFormCollapse" aria-expanded="false" aria-controls="filterFormCollapse"><i class="fa-solid fa-filter fa-fw"></i> {% trans %}filter.title{% endtrans %}</button>
|
||||
</div>
|
||||
<div id="filterFormCollapse" class="accordion-collapse collapse" data-bs-parent="#listAccordion">
|
||||
<div class="accordion-body">
|
||||
{{ form_start(filterForm, {"attr": {"data-controller": "helpers--form-cleanup", "data-action": "helpers--form-cleanup#submit"}}) }}
|
||||
|
||||
{{ form_row(filterForm.name) }}
|
||||
{{ form_row(filterForm.attachmentType) }}
|
||||
{{ form_row(filterForm.targetType) }}
|
||||
{{ form_row(filterForm.showInTable) }}
|
||||
{{ form_row(filterForm.lastModified) }}
|
||||
{{ form_row(filterForm.addedDate) }}
|
||||
{{ form_row(filterForm.dbId) }}
|
||||
|
||||
{{ form_row(filterForm.submit) }}
|
||||
{{ form_row(filterForm.discard) }}
|
||||
|
||||
<div class="row mb-3">
|
||||
<div class="col-sm-9 offset-sm-3">
|
||||
<button type="button" class="btn btn-danger" {{ stimulus_action('helpers/form_cleanup', 'clearAll') }}>{% trans %}filter.clear_filters{% endtrans %}</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ form_end(filterForm) }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{{ datatables.datatable(datatable) }}
|
||||
{% endblock %}
|
|
@ -9693,5 +9693,11 @@ Element 3</target>
|
|||
<target>Enabled search options</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="biszSr8" name="attachment.table.element_type">
|
||||
<segment>
|
||||
<source>attachment.table.element_type</source>
|
||||
<target>Associated element type</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue