From c9151c65bafdb22a701aeb5423344e12e4a0611a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jan=20B=C3=B6hmer?= Date: Sun, 21 Aug 2022 01:34:17 +0200 Subject: [PATCH] Implemented a filter constraint for entities --- .../elements/selectpicker_controller.js | 2 +- assets/css/lib/boostrap-select.css | 491 ++++++++++++++++++ src/Controller/PartListsController.php | 6 +- .../Filters/Constraints/EntityConstraint.php | 180 +++++++ .../Filters/Constraints/FilterTrait.php | 2 +- .../Filters/Constraints/NumberConstraint.php | 4 +- .../Filters/Constraints/TextConstraint.php | 2 +- src/DataTables/Filters/PartFilter.php | 42 +- .../StructuralEntityConstraintType.php | 55 ++ src/Form/Filters/PartFilterType.php | 7 + src/Services/Trees/NodesListBuilder.php | 18 +- templates/Form/FilterTypesLayout.html.twig | 4 + translations/messages.en.xlf | 24 + 13 files changed, 810 insertions(+), 27 deletions(-) create mode 100644 assets/css/lib/boostrap-select.css create mode 100644 src/DataTables/Filters/Constraints/EntityConstraint.php create mode 100644 src/Form/Filters/Constraints/StructuralEntityConstraintType.php diff --git a/assets/controllers/elements/selectpicker_controller.js b/assets/controllers/elements/selectpicker_controller.js index 182a2e35..aac08719 100644 --- a/assets/controllers/elements/selectpicker_controller.js +++ b/assets/controllers/elements/selectpicker_controller.js @@ -2,7 +2,7 @@ const bootstrap = window.bootstrap = require('bootstrap'); // without this boots require('bootstrap-select/js/bootstrap-select'); // we have to manually require the working js file import {Controller} from "@hotwired/stimulus"; -import "bootstrap-select/dist/css/bootstrap-select.css"; +import "../../css/lib/boostrap-select.css"; import "../../css/selectpicker_extensions.css"; export default class extends Controller { diff --git a/assets/css/lib/boostrap-select.css b/assets/css/lib/boostrap-select.css new file mode 100644 index 00000000..7ae65947 --- /dev/null +++ b/assets/css/lib/boostrap-select.css @@ -0,0 +1,491 @@ +/*! + * Bootstrap-select v1.14.0-beta3 (https://developer.snapappointments.com/bootstrap-select) + * + * Copyright 2012-2022 SnapAppointments, LLC + * Licensed under MIT (https://github.com/snapappointments/bootstrap-select/blob/master/LICENSE) + */ + +/** + * Modified by Jan Böhmer 2022, to improve styling with BS5: + * - Keep border around selectpicker in a form-control + */ + +@-webkit-keyframes bs-notify-fadeOut { + 0% { + opacity: 0.9; + } + 100% { + opacity: 0; + } +} +@-o-keyframes bs-notify-fadeOut { + 0% { + opacity: 0.9; + } + 100% { + opacity: 0; + } +} +@keyframes bs-notify-fadeOut { + 0% { + opacity: 0.9; + } + 100% { + opacity: 0; + } +} +select.bs-select-hidden, +.bootstrap-select > select.bs-select-hidden, +select.selectpicker { + display: none !important; +} +.bootstrap-select { + width: 220px; + vertical-align: middle; +} +.bootstrap-select > .dropdown-toggle { + position: relative; + width: 100%; + text-align: right; + white-space: nowrap; + display: -webkit-inline-box; + display: -webkit-inline-flex; + display: -ms-inline-flexbox; + display: inline-flex; + -webkit-box-align: center; + -webkit-align-items: center; + -ms-flex-align: center; + align-items: center; + -webkit-box-pack: justify; + -webkit-justify-content: space-between; + -ms-flex-pack: justify; + justify-content: space-between; +} +.bootstrap-select > .dropdown-toggle:after { + margin-top: -1px; +} +.bootstrap-select > .dropdown-toggle.bs-placeholder, +.bootstrap-select > .dropdown-toggle.bs-placeholder:hover, +.bootstrap-select > .dropdown-toggle.bs-placeholder:focus, +.bootstrap-select > .dropdown-toggle.bs-placeholder:active { + color: #999; +} +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-primary, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-secondary, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-success, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-danger, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-info, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-dark, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-primary:hover, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-secondary:hover, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-success:hover, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-danger:hover, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-info:hover, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-dark:hover, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-primary:focus, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-secondary:focus, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-success:focus, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-danger:focus, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-info:focus, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-dark:focus, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-primary:active, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-secondary:active, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-success:active, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-danger:active, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-info:active, +.bootstrap-select > .dropdown-toggle.bs-placeholder.btn-dark:active { + color: rgba(255, 255, 255, 0.5); +} +.bootstrap-select > select { + position: absolute !important; + bottom: 0; + left: 50%; + display: block !important; + width: 0.5px !important; + height: 100% !important; + padding: 0 !important; + opacity: 0 !important; + border: none; + z-index: 0 !important; +} +.bootstrap-select > select.mobile-device { + top: 0; + left: 0; + display: block !important; + width: 100% !important; + z-index: 2 !important; +} +.has-error .bootstrap-select .dropdown-toggle, +.error .bootstrap-select .dropdown-toggle, +.bootstrap-select.is-invalid .dropdown-toggle, +.was-validated .bootstrap-select select:invalid + .dropdown-toggle { + border-color: #b94a48; +} +.bootstrap-select.is-valid .dropdown-toggle, +.was-validated .bootstrap-select select:valid + .dropdown-toggle { + border-color: #28a745; +} +.bootstrap-select.fit-width { + width: auto !important; +} +.bootstrap-select:not([class*="col-"]):not([class*="form-control"]):not(.input-group-btn) { + width: 220px; +} +.bootstrap-select > select.mobile-device:focus + .dropdown-toggle, +.bootstrap-select .dropdown-toggle:focus { + outline: thin dotted #333333 !important; + outline: 5px auto -webkit-focus-ring-color !important; + outline-offset: -2px; +} +.bootstrap-select.form-control { + margin-bottom: 0; + padding: 0; + /*border: none;*/ + height: auto; +} +:not(.input-group) > .bootstrap-select.form-control:not([class*="col-"]) { + width: 100%; +} +.bootstrap-select.form-control.input-group-btn { + float: none; + z-index: auto; +} +.form-inline .bootstrap-select, +.form-inline .bootstrap-select.form-control:not([class*="col-"]) { + width: auto; +} +.bootstrap-select:not(.input-group-btn), +.bootstrap-select[class*="col-"] { + float: none; + display: inline-block; + margin-left: 0; +} +.bootstrap-select.dropdown-menu-right, +.bootstrap-select[class*="col-"].dropdown-menu-right, +.row .bootstrap-select[class*="col-"].dropdown-menu-right { + float: right; +} +.form-inline .bootstrap-select, +.form-horizontal .bootstrap-select, +.form-group .bootstrap-select { + margin-bottom: 0; +} +.form-group-lg .bootstrap-select.form-control, +.form-group-sm .bootstrap-select.form-control { + padding: 0; +} +.form-group-lg .bootstrap-select.form-control .dropdown-toggle, +.form-group-sm .bootstrap-select.form-control .dropdown-toggle { + height: 100%; + font-size: inherit; + line-height: inherit; + border-radius: inherit; +} +.bootstrap-select.form-control-sm .dropdown-toggle, +.bootstrap-select.form-control-lg .dropdown-toggle { + font-size: inherit; + line-height: inherit; + border-radius: inherit; +} +.bootstrap-select.form-control-sm .dropdown-toggle { + padding: 0.25rem 0.5rem; +} +.bootstrap-select.form-control-lg .dropdown-toggle { + padding: 0.5rem 1rem; +} +.form-inline .bootstrap-select .form-control { + width: 100%; +} +.bootstrap-select.disabled, +.bootstrap-select > .disabled { + cursor: not-allowed; +} +.bootstrap-select.disabled:focus, +.bootstrap-select > .disabled:focus { + outline: none !important; +} +.bootstrap-select.bs-container { + position: absolute; + top: 0; + left: 0; + height: 0 !important; + padding: 0 !important; +} +.bootstrap-select.bs-container .dropdown-menu { + z-index: 1060; +} +.bootstrap-select .dropdown-toggle .filter-option { + position: static; + top: 0; + left: 0; + float: left; + height: 100%; + width: 100%; + text-align: left; + overflow: hidden; + -webkit-box-flex: 0; + -webkit-flex: 0 1 auto; + -ms-flex: 0 1 auto; + flex: 0 1 auto; +} +.bs3.bootstrap-select .dropdown-toggle .filter-option { + padding-right: inherit; +} +.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option { + position: absolute; + padding-top: inherit; + padding-bottom: inherit; + padding-left: inherit; + float: none; +} +.input-group .bs3-has-addon.bootstrap-select .dropdown-toggle .filter-option .filter-option-inner { + padding-right: inherit; +} +.bootstrap-select .dropdown-toggle .filter-option-inner-inner { + overflow: hidden; +} +.bootstrap-select .dropdown-toggle .filter-expand { + width: 0 !important; + float: left; + opacity: 0 !important; + overflow: hidden; +} +.bootstrap-select .dropdown-toggle .caret { + position: absolute; + top: 50%; + right: 12px; + margin-top: -2px; + vertical-align: middle; +} +.bootstrap-select .dropdown-toggle .bs-select-clear-selected { + position: relative; + display: block; + margin-right: 5px; + text-align: center; +} +.bs3.bootstrap-select .dropdown-toggle .bs-select-clear-selected { + padding-right: inherit; +} +.bootstrap-select .dropdown-toggle .bs-select-clear-selected span { + position: relative; + top: -webkit-calc(((-1em / 1.5) + 1ex) / 2); + top: calc(((-1em / 1.5) + 1ex) / 2); + pointer-events: none; +} +.bs3.bootstrap-select .dropdown-toggle .bs-select-clear-selected span { + top: auto; +} +.bootstrap-select .dropdown-toggle.bs-placeholder .bs-select-clear-selected { + display: none; +} +.input-group .bootstrap-select.form-control .dropdown-toggle { + border-radius: inherit; +} +.bootstrap-select[class*="col-"] .dropdown-toggle { + width: 100%; +} +.bootstrap-select .dropdown-menu { + min-width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.bootstrap-select .dropdown-menu > .inner:focus { + outline: none !important; +} +.bootstrap-select .dropdown-menu.inner { + position: static; + float: none; + border: 0; + padding: 0; + margin: 0; + border-radius: 0; + -webkit-box-shadow: none; + box-shadow: none; +} +.bootstrap-select .dropdown-menu li { + position: relative; +} +.bootstrap-select .dropdown-menu li.active small { + color: rgba(255, 255, 255, 0.5) !important; +} +.bootstrap-select .dropdown-menu li.disabled a { + cursor: not-allowed; +} +.bootstrap-select .dropdown-menu li a { + cursor: pointer; + -webkit-user-select: none; + -moz-user-select: none; + -ms-user-select: none; + user-select: none; +} +.bootstrap-select .dropdown-menu li a.opt { + position: relative; + padding-left: 2.25em; +} +.bootstrap-select .dropdown-menu li a span.check-mark { + display: none; +} +.bootstrap-select .dropdown-menu li a span.text { + display: inline-block; +} +.bootstrap-select .dropdown-menu li small { + padding-left: 0.5em; +} +.bootstrap-select .dropdown-menu .notify { + position: absolute; + bottom: 5px; + width: 96%; + margin: 0 2%; + min-height: 26px; + padding: 3px 5px; + background: #f5f5f5; + border: 1px solid #e3e3e3; + -webkit-box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.05); + pointer-events: none; + opacity: 0.9; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.bootstrap-select .dropdown-menu .notify.fadeOut { + -webkit-animation: 300ms linear 750ms forwards bs-notify-fadeOut; + -o-animation: 300ms linear 750ms forwards bs-notify-fadeOut; + animation: 300ms linear 750ms forwards bs-notify-fadeOut; +} +.bootstrap-select .no-results { + padding: 3px; + background: #f5f5f5; + margin: 0 5px; + white-space: nowrap; +} +.bootstrap-select.fit-width .dropdown-toggle .filter-option { + position: static; + display: inline; + padding: 0; +} +.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner, +.bootstrap-select.fit-width .dropdown-toggle .filter-option-inner-inner { + display: inline; +} +.bootstrap-select.fit-width .dropdown-toggle .bs-caret:before { + content: '\00a0'; +} +.bootstrap-select.fit-width .dropdown-toggle .caret { + position: static; + top: auto; + margin-top: -1px; +} +.bootstrap-select.show-tick .dropdown-menu .selected span.check-mark { + position: absolute; + display: inline-block; + right: 15px; + top: 5px; +} +.bootstrap-select.show-tick .dropdown-menu li a span.text { + margin-right: 34px; +} +.bootstrap-select .bs-ok-default:after { + content: ''; + display: block; + width: 0.5em; + height: 1em; + border-style: solid; + border-width: 0 0.26em 0.26em 0; + -webkit-transform-style: preserve-3d; + transform-style: preserve-3d; + -webkit-transform: rotate(45deg); + -ms-transform: rotate(45deg); + -o-transform: rotate(45deg); + transform: rotate(45deg); +} +.bootstrap-select.show-menu-arrow.open > .dropdown-toggle, +.bootstrap-select.show-menu-arrow.show > .dropdown-toggle { + z-index: 1061; +} +.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:before { + content: ''; + border-left: 7px solid transparent; + border-right: 7px solid transparent; + border-bottom: 7px solid rgba(204, 204, 204, 0.2); + position: absolute; + bottom: -4px; + left: 9px; + display: none; +} +.bootstrap-select.show-menu-arrow .dropdown-toggle .filter-option:after { + content: ''; + border-left: 6px solid transparent; + border-right: 6px solid transparent; + border-bottom: 6px solid white; + position: absolute; + bottom: -4px; + left: 10px; + display: none; +} +.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:before { + bottom: auto; + top: -4px; + border-top: 7px solid rgba(204, 204, 204, 0.2); + border-bottom: 0; +} +.bootstrap-select.show-menu-arrow.dropup .dropdown-toggle .filter-option:after { + bottom: auto; + top: -4px; + border-top: 6px solid white; + border-bottom: 0; +} +.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:before { + right: 12px; + left: auto; +} +.bootstrap-select.show-menu-arrow.pull-right .dropdown-toggle .filter-option:after { + right: 13px; + left: auto; +} +.bootstrap-select.show-menu-arrow.open > .dropdown-toggle .filter-option:before, +.bootstrap-select.show-menu-arrow.show > .dropdown-toggle .filter-option:before, +.bootstrap-select.show-menu-arrow.open > .dropdown-toggle .filter-option:after, +.bootstrap-select.show-menu-arrow.show > .dropdown-toggle .filter-option:after { + display: block; +} +.bs-searchbox, +.bs-actionsbox, +.bs-donebutton { + padding: 4px 8px; +} +.bs-actionsbox { + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.bs-actionsbox .btn-group { + display: block; +} +.bs-actionsbox .btn-group button { + width: 50%; +} +.bs-donebutton { + float: left; + width: 100%; + -webkit-box-sizing: border-box; + -moz-box-sizing: border-box; + box-sizing: border-box; +} +.bs-donebutton .btn-group { + display: block; +} +.bs-donebutton .btn-group button { + width: 100%; +} +.bs-searchbox + .bs-actionsbox { + padding: 0 8px 4px; +} +.bs-searchbox .form-control { + margin-bottom: 0; + width: 100%; + float: none; +} +/*# sourceMappingURL=bootstrap-select.css.map */ \ No newline at end of file diff --git a/src/Controller/PartListsController.php b/src/Controller/PartListsController.php index 070db537..116edaab 100644 --- a/src/Controller/PartListsController.php +++ b/src/Controller/PartListsController.php @@ -51,6 +51,7 @@ use App\Entity\Parts\Storelocation; use App\Entity\Parts\Supplier; use App\Form\Filters\PartFilterType; use App\Services\Parts\PartsTableActionHandler; +use App\Services\Trees\NodesListBuilder; use Doctrine\ORM\EntityManagerInterface; use Omines\DataTablesBundle\DataTableFactory; use Symfony\Bundle\FrameworkBundle\Controller\AbstractController; @@ -260,7 +261,6 @@ class PartListsController extends AbstractController return $this->render('Parts/lists/search_list.html.twig', [ 'datatable' => $table, 'keyword' => $search, - 'filterForm' => $filterForm->createView() ]); } @@ -269,12 +269,12 @@ class PartListsController extends AbstractController * * @return JsonResponse|Response */ - public function showAll(Request $request, DataTableFactory $dataTable) + public function showAll(Request $request, DataTableFactory $dataTable, NodesListBuilder $nodesListBuilder) { $formRequest = clone $request; $formRequest->setMethod('GET'); - $filter = new PartFilter(); + $filter = new PartFilter($nodesListBuilder); $filterForm = $this->createForm(PartFilterType::class, $filter, ['method' => 'GET']); $filterForm->handleRequest($formRequest); diff --git a/src/DataTables/Filters/Constraints/EntityConstraint.php b/src/DataTables/Filters/Constraints/EntityConstraint.php new file mode 100644 index 00000000..0e2ec7ce --- /dev/null +++ b/src/DataTables/Filters/Constraints/EntityConstraint.php @@ -0,0 +1,180 @@ + The class to use for the comparison + */ + protected $class; + + /** + * @var string|null The operator to use + */ + protected $operator; + + /** + * @var T The value to compare to + */ + protected $value; + + /** + * @param NodesListBuilder $nodesListBuilder + * @param class-string $class + * @param string $property + * @param string|null $identifier + * @param $value + * @param string $operator + */ + public function __construct(NodesListBuilder $nodesListBuilder, string $class, string $property, string $identifier = null, $value = null, string $operator = '') + { + $this->nodesListBuilder = $nodesListBuilder; + $this->class = $class; + + parent::__construct($property, $identifier); + $this->value = $value; + $this->operator = $operator; + } + + public function getClass(): string + { + return $this->class; + } + + /** + * @return string|null + */ + public function getOperator(): ?string + { + return $this->operator; + } + + /** + * @param string|null $operator + */ + public function setOperator(?string $operator): self + { + $this->operator = $operator; + return $this; + } + + /** + * @return mixed|null + */ + public function getValue(): ?AbstractDBElement + { + return $this->value; + } + + /** + * @param T|null $value + */ + public function setValue(?AbstractDBElement $value): void + { + if (!$value instanceof $this->class) { + throw new \InvalidArgumentException('The value must be an instance of ' . $this->class); + } + + $this->value = $value; + } + + /** + * Checks whether the constraints apply to a structural type or not + * @return bool + */ + public function isStructural(): bool + { + return is_subclass_of($this->class, AbstractStructuralDBElement::class); + } + + /** + * Returns a list of operators which are allowed with the given class. + * @return string[] + */ + public function getAllowedOperatorValues(): array + { + //Base operators are allowed for everything + $tmp = self::ALLOWED_OPERATOR_VALUES_BASE; + + if ($this->isStructural()) { + $tmp = array_merge($tmp, self::ALLOWED_OPERATOR_VALUES_STRUCTURAL); + } + + return $tmp; + } + + 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, $this->getAllowedOperatorValues(), true)) { + throw new \RuntimeException('Invalid operator '. $this->operator . ' provided. Valid operators are '. implode(', ', $this->getAllowedOperatorValues())); + } + + //We need to handle null values differently, as they can not be compared with == or != + if ($this->value === null) { + if($this->operator === '=' || $this->operator === 'INCLUDING_CHILDREN') { + $queryBuilder->andWhere(sprintf("%s IS NULL", $this->property)); + return; + } + + if ($this->operator === '!=' || $this->operator === 'EXCLUDING_CHILDREN') { + $queryBuilder->andWhere(sprintf("%s IS NOT NULL", $this->property)); + return; + } + + throw new \RuntimeException('Unknown operator '. $this->operator . ' provided. Valid operators are '. implode(', ', $this->getAllowedOperatorValues())); + } + + if($this->operator === '=' || $this->operator === '!=') { + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, $this->operator, $this->value); + return; + } + + //Otherwise retrieve the children list and apply the operator to it + if($this->isStructural()) { + $list = $this->nodesListBuilder->getChildrenFlatList($this->value); + //Add the element itself to the list + $list[] = $this->value; + + if ($this->operator === 'INCLUDING_CHILDREN') { + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'IN', $list); + return; + } + + if ($this->operator === 'EXCLUDING_CHILDREN') { + $this->addSimpleAndConstraint($queryBuilder, $this->property, $this->identifier, 'NOT IN', $list); + return; + } + } else { + throw new \RuntimeException('Cannot apply operator '. $this->operator . ' to non-structural type'); + } + + } +} \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/FilterTrait.php b/src/DataTables/Filters/Constraints/FilterTrait.php index 3823ef89..4cb9939e 100644 --- a/src/DataTables/Filters/Constraints/FilterTrait.php +++ b/src/DataTables/Filters/Constraints/FilterTrait.php @@ -27,7 +27,7 @@ trait FilterTrait */ protected function addSimpleAndConstraint(QueryBuilder $queryBuilder, string $property, string $parameterIdentifier, string $comparison_operator, $value): void { - $queryBuilder->andWhere(sprintf("%s %s :%s", $property, $comparison_operator, $parameterIdentifier)); + $queryBuilder->andWhere(sprintf("%s %s (:%s)", $property, $comparison_operator, $parameterIdentifier)); $queryBuilder->setParameter($parameterIdentifier, $value); } } \ No newline at end of file diff --git a/src/DataTables/Filters/Constraints/NumberConstraint.php b/src/DataTables/Filters/Constraints/NumberConstraint.php index 92ef51ad..fa1b073d 100644 --- a/src/DataTables/Filters/Constraints/NumberConstraint.php +++ b/src/DataTables/Filters/Constraints/NumberConstraint.php @@ -76,7 +76,7 @@ class NumberConstraint extends AbstractConstraint } - public function __construct(string $property, string $identifier = null, $value1 = null, $operator = null, $value2 = null) + public function __construct(string $property, string $identifier = null, $value1 = null, string $operator = null, $value2 = null) { parent::__construct($property, $identifier); $this->value1 = $value1; @@ -93,7 +93,7 @@ class NumberConstraint extends AbstractConstraint public function apply(QueryBuilder $queryBuilder): void { //If no value is provided then we do not apply a filter - if ($this->value1 === null) { + if (!$this->isEnabled()) { return; } diff --git a/src/DataTables/Filters/Constraints/TextConstraint.php b/src/DataTables/Filters/Constraints/TextConstraint.php index f4cf9b40..3ce5c7eb 100644 --- a/src/DataTables/Filters/Constraints/TextConstraint.php +++ b/src/DataTables/Filters/Constraints/TextConstraint.php @@ -19,7 +19,7 @@ class TextConstraint extends AbstractConstraint */ protected $value; - public function __construct(string $property, string $identifier = null, $value = null, $operator = '') + public function __construct(string $property, string $identifier = null, $value = null, string $operator = '') { parent::__construct($property, $identifier); $this->value = $value; diff --git a/src/DataTables/Filters/PartFilter.php b/src/DataTables/Filters/PartFilter.php index 66770e76..4fccbcc4 100644 --- a/src/DataTables/Filters/PartFilter.php +++ b/src/DataTables/Filters/PartFilter.php @@ -4,8 +4,11 @@ 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\NumberConstraint; use App\DataTables\Filters\Constraints\TextConstraint; +use App\Entity\Parts\Category; +use App\Services\Trees\NodesListBuilder; use Doctrine\ORM\QueryBuilder; class PartFilter implements FilterInterface @@ -34,6 +37,28 @@ class PartFilter implements FilterInterface /** @var DateTimeConstraint */ protected $addedDate; + /** @var EntityConstraint */ + protected $category; + + public function __construct(NodesListBuilder $nodesListBuilder) + { + $this->favorite = + $this->needsReview = new BooleanConstraint('part.needs_review'); + $this->mass = new NumberConstraint('part.mass'); + $this->name = new TextConstraint('part.name'); + $this->description = new TextConstraint('part.description'); + $this->addedDate = new DateTimeConstraint('part.addedDate'); + $this->lastModified = new DateTimeConstraint('part.lastModified'); + + $this->category = new EntityConstraint($nodesListBuilder, Category::class, 'part.category'); + } + + public function apply(QueryBuilder $queryBuilder): void + { + $this->applyAllChildFilters($queryBuilder); + } + + /** * @return BooleanConstraint|false */ @@ -81,21 +106,8 @@ class PartFilter implements FilterInterface return $this->addedDate; } - - - public function __construct() + public function getCategory(): EntityConstraint { - $this->favorite = - $this->needsReview = new BooleanConstraint('part.needs_review'); - $this->mass = new NumberConstraint('part.mass'); - $this->name = new TextConstraint('part.name'); - $this->description = new TextConstraint('part.description'); - $this->addedDate = new DateTimeConstraint('part.addedDate'); - $this->lastModified = new DateTimeConstraint('part.lastModified'); - } - - public function apply(QueryBuilder $queryBuilder): void - { - $this->applyAllChildFilters($queryBuilder); + return $this->category; } } diff --git a/src/Form/Filters/Constraints/StructuralEntityConstraintType.php b/src/Form/Filters/Constraints/StructuralEntityConstraintType.php new file mode 100644 index 00000000..4730caec --- /dev/null +++ b/src/Form/Filters/Constraints/StructuralEntityConstraintType.php @@ -0,0 +1,55 @@ +setDefaults([ + 'compound' => true, + 'data_class' => EntityConstraint::class, + 'text_suffix' => '', // An suffix which is attached as text-append to the input group. This can for example be used for units + ]); + + $resolver->setRequired('entity_class'); + } + + public function buildForm(FormBuilderInterface $builder, array $options): void + { + $choices = [ + '' => '', + 'filter.entity_constraint.operator.EQ' => '=', + 'filter.entity_constraint.operator.NEQ' => '!=', + 'filter.entity_constraint.operator.INCLUDING_CHILDREN' => 'INCLUDING_CHILDREN', + 'filter.entity_constraint.operator.EXCLUDING_CHILDREN' => 'EXCLUDING_CHILDREN', + ]; + + $builder->add('value', StructuralEntityType::class, [ + 'class' => $options['entity_class'], + 'required' => false, + ]); + + + $builder->add('operator', ChoiceType::class, [ + 'label' => 'filter.text_constraint.operator', + 'choices' => $choices, + 'required' => false, + ]); + } + + public function buildView(FormView $view, FormInterface $form, array $options) + { + parent::buildView($view, $form, $options); + $view->vars['text_suffix'] = $options['text_suffix']; + } +} \ No newline at end of file diff --git a/src/Form/Filters/PartFilterType.php b/src/Form/Filters/PartFilterType.php index ffa76976..6c63cc15 100644 --- a/src/Form/Filters/PartFilterType.php +++ b/src/Form/Filters/PartFilterType.php @@ -3,9 +3,11 @@ namespace App\Form\Filters; use App\DataTables\Filters\PartFilter; +use App\Entity\Parts\Category; 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\TextConstraintType; use Symfony\Component\Form\AbstractType; use Symfony\Component\Form\Extension\Core\Type\DateTimeType; @@ -27,6 +29,11 @@ class PartFilterType extends AbstractType public function buildForm(FormBuilderInterface $builder, array $options) { + $builder->add('category', StructuralEntityConstraintType::class, [ + 'label' => 'part.edit.category', + 'entity_class' => Category::class + ]); + $builder->add('favorite', BooleanConstraintType::class, [ 'label' => 'part.edit.is_favorite' ]); diff --git a/src/Services/Trees/NodesListBuilder.php b/src/Services/Trees/NodesListBuilder.php index 3bb13486..b2d2c159 100644 --- a/src/Services/Trees/NodesListBuilder.php +++ b/src/Services/Trees/NodesListBuilder.php @@ -84,10 +84,20 @@ class NodesListBuilder return $this->cache->get($key, function (ItemInterface $item) use ($class_name, $parent, $secure_class_name) { // Invalidate when groups, a element with the class or the user changes $item->tag(['groups', 'tree_list', $this->keyGenerator->generateKey(), $secure_class_name]); - /** @var StructuralDBElementRepository */ - $repo = $this->em->getRepository($class_name); - - return $repo->toNodesList($parent); + return $this->em->getRepository($class_name)->toNodesList($parent); }); } + + /** + * Returns a flattened list of all (recursive) children elements of the given AbstractStructuralDBElement. + * The value is cached for performance reasons. + * + * @template T + * @param T $element + * @return T[] + */ + public function getChildrenFlatList(AbstractStructuralDBElement $element): array + { + return $this->typeToNodesList(get_class($element), $element); + } } diff --git a/templates/Form/FilterTypesLayout.html.twig b/templates/Form/FilterTypesLayout.html.twig index 25a23e0f..776700b5 100644 --- a/templates/Form/FilterTypesLayout.html.twig +++ b/templates/Form/FilterTypesLayout.html.twig @@ -24,6 +24,10 @@ {% endblock %} +{% block structural_entity_constraint_widget %} + {{ block('text_constraint_widget') }} +{% endblock %} + {% block date_time_constraint_widget %} {{ block('number_constraint_widget') }} {% endblock %} \ No newline at end of file diff --git a/translations/messages.en.xlf b/translations/messages.en.xlf index 13fb5de1..fe581953 100644 --- a/translations/messages.en.xlf +++ b/translations/messages.en.xlf @@ -9411,5 +9411,29 @@ Element 3 and + + + filter.entity_constraint.operator.EQ + Is (excluding children) + + + + + filter.entity_constraint.operator.NEQ + Is not (exluding children) + + + + + filter.entity_constraint.operator.INCLUDING_CHILDREN + Is (including children) + + + + + filter.entity_constraint.operator.EXCLUDING_CHILDREN + Is not (excluding children) + +