mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 01:25:55 +02:00
Implemented a filter constraint for entities
This commit is contained in:
parent
0bc9d8cba1
commit
c9151c65ba
13 changed files with 810 additions and 27 deletions
|
@ -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 {
|
||||
|
|
491
assets/css/lib/boostrap-select.css
Normal file
491
assets/css/lib/boostrap-select.css
Normal file
|
@ -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 */
|
|
@ -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);
|
||||
|
||||
|
|
180
src/DataTables/Filters/Constraints/EntityConstraint.php
Normal file
180
src/DataTables/Filters/Constraints/EntityConstraint.php
Normal file
|
@ -0,0 +1,180 @@
|
|||
<?php
|
||||
|
||||
namespace App\DataTables\Filters\Constraints;
|
||||
|
||||
use App\Entity\Base\AbstractDBElement;
|
||||
use App\Entity\Base\AbstractStructuralDBElement;
|
||||
use App\Services\Trees\NodesListBuilder;
|
||||
use Doctrine\ORM\QueryBuilder;
|
||||
|
||||
/**
|
||||
* @template T
|
||||
*/
|
||||
class EntityConstraint extends AbstractConstraint
|
||||
{
|
||||
private const ALLOWED_OPERATOR_VALUES_BASE = ['=', '!='];
|
||||
private const ALLOWED_OPERATOR_VALUES_STRUCTURAL = ['INCLUDING_CHILDREN', 'EXCLUDING_CHILDREN'];
|
||||
|
||||
/**
|
||||
* @var
|
||||
*/
|
||||
protected $nodesListBuilder;
|
||||
|
||||
/**
|
||||
* @var class-string<T> 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<T> $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');
|
||||
}
|
||||
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
namespace App\Form\Filters\Constraints;
|
||||
|
||||
use App\DataTables\Filters\Constraints\EntityConstraint;
|
||||
use App\Form\Type\StructuralEntityType;
|
||||
use Symfony\Component\Form\AbstractType;
|
||||
use Symfony\Component\Form\Extension\Core\Type\ChoiceType;
|
||||
use Symfony\Component\Form\FormBuilderInterface;
|
||||
use Symfony\Component\Form\FormInterface;
|
||||
use Symfony\Component\Form\FormView;
|
||||
use Symfony\Component\OptionsResolver\OptionsResolver;
|
||||
|
||||
class StructuralEntityConstraintType extends AbstractType
|
||||
{
|
||||
public function configureOptions(OptionsResolver $resolver): void
|
||||
{
|
||||
$resolver->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'];
|
||||
}
|
||||
}
|
|
@ -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'
|
||||
]);
|
||||
|
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -24,6 +24,10 @@
|
|||
</div>
|
||||
{% endblock %}
|
||||
|
||||
{% block structural_entity_constraint_widget %}
|
||||
{{ block('text_constraint_widget') }}
|
||||
{% endblock %}
|
||||
|
||||
{% block date_time_constraint_widget %}
|
||||
{{ block('number_constraint_widget') }}
|
||||
{% endblock %}
|
|
@ -9411,5 +9411,29 @@ Element 3</target>
|
|||
<target>and</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="NOmRxFG" name="filter.entity_constraint.operator.EQ">
|
||||
<segment>
|
||||
<source>filter.entity_constraint.operator.EQ</source>
|
||||
<target>Is (excluding children)</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="7zxBnzb" name="filter.entity_constraint.operator.NEQ">
|
||||
<segment>
|
||||
<source>filter.entity_constraint.operator.NEQ</source>
|
||||
<target>Is not (exluding children)</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="oX1nk5D" name="filter.entity_constraint.operator.INCLUDING_CHILDREN">
|
||||
<segment>
|
||||
<source>filter.entity_constraint.operator.INCLUDING_CHILDREN</source>
|
||||
<target>Is (including children)</target>
|
||||
</segment>
|
||||
</unit>
|
||||
<unit id="AS9s6LU" name="filter.entity_constraint.operator.EXCLUDING_CHILDREN">
|
||||
<segment>
|
||||
<source>filter.entity_constraint.operator.EXCLUDING_CHILDREN</source>
|
||||
<target>Is not (excluding children)</target>
|
||||
</segment>
|
||||
</unit>
|
||||
</file>
|
||||
</xliff>
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue