partkeepr

fork of partkeepr
git clone https://git.e1e0.net/partkeepr.git
Log | Files | Refs | Submodules | README | LICENSE

commit 478ba77657551e5affff690d24660aff59d71566
parent 1ea33d140627423885134e8f118167ef8ab7f80d
Author: Felicia Hummel <felicitus@felicitus.org>
Date:   Sat, 30 Jul 2016 20:28:36 +0200

Merge pull request #697 from partkeepr/PartKeepr-501

Added support for multiple fields when searching for parts
Diffstat:
Msrc/PartKeepr/DoctrineReflectionBundle/Filter/AdvancedSearchFilter.php | 193+++++++++++++++++++++++++++++++++++++++++++++++--------------------------------
Asrc/PartKeepr/DoctrineReflectionBundle/Filter/AssociationPropertyInterface.php | 12++++++++++++
Asrc/PartKeepr/DoctrineReflectionBundle/Filter/AssociationPropertyTrait.php | 50++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/DoctrineReflectionBundle/Filter/Filter.php | 148+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Asrc/PartKeepr/DoctrineReflectionBundle/Filter/PropertyFilter.php | 16++++++++++++++++
Asrc/PartKeepr/DoctrineReflectionBundle/Filter/Sorter.php | 30++++++++++++++++++++++++++++++
Asrc/PartKeepr/DoctrineReflectionBundle/Tests/AdvancedSearchFilterTest.php | 299+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Editor/EditorGrid.js | 10+++++++++-
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartsGrid.js | 1+
Msrc/PartKeepr/FrontendBundle/Resources/public/js/Util/Filter.js | 67++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-
Msrc/PartKeepr/FrontendBundle/Resources/public/js/form/field/SearchField.js | 54+++++++++++++++++++++++++++++++++++++++---------------
Msrc/PartKeepr/PartBundle/DataFixtures/PartDataLoader.php | 2+-
Msrc/PartKeepr/StorageLocationBundle/DataFixtures/StorageLocationLoader.php | 6++++++
13 files changed, 791 insertions(+), 97 deletions(-)

diff --git a/src/PartKeepr/DoctrineReflectionBundle/Filter/AdvancedSearchFilter.php b/src/PartKeepr/DoctrineReflectionBundle/Filter/AdvancedSearchFilter.php @@ -37,26 +37,6 @@ class AdvancedSearchFilter extends AbstractFilter */ private $propertyAccessor; - const OPERATOR_LESS_THAN = '<'; - const OPERATOR_GREATER_THAN = '>'; - const OPERATOR_EQUALS = '='; - const OPERATOR_GREATER_THAN_EQUALS = '>='; - const OPERATOR_LESS_THAN_EQUALS = '>='; - const OPERATOR_NOT_EQUALS = '!='; - const OPERATOR_IN = 'in'; - const OPERATOR_LIKE = 'like'; - - const OPERATORS = [ - self::OPERATOR_LESS_THAN, - self::OPERATOR_GREATER_THAN, - self::OPERATOR_EQUALS, - self::OPERATOR_GREATER_THAN_EQUALS, - self::OPERATOR_LESS_THAN_EQUALS, - self::OPERATOR_NOT_EQUALS, - self::OPERATOR_IN, - self::OPERATOR_LIKE, - ]; - private $aliases = []; private $parameterCount = 0; @@ -69,6 +49,7 @@ class AdvancedSearchFilter extends AbstractFilter * @param ManagerRegistry $managerRegistry * @param IriConverterInterface $iriConverter * @param PropertyAccessorInterface $propertyAccessor + * @param RequestStack $requestStack * @param null|array $properties Null to allow filtering on all properties with the exact strategy * or a map of property name with strategy. */ @@ -104,37 +85,82 @@ class AdvancedSearchFilter extends AbstractFilter } $properties = $this->extractProperties($request); + $filters = $properties['filters']; $sorters = $properties['sorters']; foreach ($filters as $filter) { - if (isset($fieldNames[$filter['property']]) && $filter['association'] === null) { - $queryBuilder - ->andWhere( + /** + * @var $filter Filter + */ + if (isset($fieldNames[$filter->getProperty()]) && $filter->getAssociation() === null) { + if ($filter->hasSubFilters()) { + $subFilterExpressions = []; + + foreach ($filter->getSubFilters() as $subFilter) { + /** + * @var $subFilter Filter + */ + if ($subFilter->getAssociation() !== null) { + $this->addJoins($queryBuilder, $subFilter); + } + + $subFilterExpressions[] = $this->getFilterExpression($queryBuilder, $subFilter); + } + + $expressions = call_user_func_array(array($queryBuilder->expr(), "orX"), $subFilterExpressions); + $queryBuilder->andWhere($expressions); + } else { + $queryBuilder->andWhere( $this->getFilterExpression($queryBuilder, $filter) ); + } + } else { - if ($filter['association'] !== null) { + if ($filter->getAssociation() !== null) { // Pull in associations $this->addJoins($queryBuilder, $filter); } - $filter['value'] = $this->getFilterValueFromUrl($filter['value']); + $filter->setValue($this->getFilterValueFromUrl($filter->getValue())); + + if ($filter->hasSubFilters()) { + $subFilterExpressions = []; + + foreach ($filter->getSubFilters() as $subFilter) { + /** + * @var $subFilter Filter + */ + if ($subFilter->getAssociation() !== null) { + $this->addJoins($queryBuilder, $subFilter); + } + + $subFilterExpressions[] = $this->getFilterExpression($queryBuilder, $subFilter); + } + + $expressions = call_user_func_array(array($queryBuilder->expr(), "orX"), $subFilterExpressions); + $queryBuilder->andWhere($expressions); + } else { + $queryBuilder->andWhere( + $this->getFilterExpression($queryBuilder, $filter) + ); + } - $queryBuilder->andWhere( - $this->getFilterExpression($queryBuilder, $filter) - ); } } foreach ($sorters as $sorter) { - if ($sorter['association'] !== null) { + /** + * @var $sorter Sorter + */ + if ($sorter->getAssociation() !== null) { // Pull in associations $this->addJoins($queryBuilder, $sorter); } $this->applyOrderByExpression($queryBuilder, $sorter); } + } /** @@ -181,14 +207,14 @@ class AdvancedSearchFilter extends AbstractFilter * @param QueryBuilder $queryBuilder * @param $filter */ - private function addJoins(QueryBuilder $queryBuilder, $filter) + private function addJoins(QueryBuilder $queryBuilder, AssociationPropertyInterface $filter) { - if (in_array($filter['association'], $this->joins)) { + if (in_array($filter->getAssociation(), $this->joins)) { // Association already added, return return; } - $associations = explode('.', $filter['association']); + $associations = explode('.', $filter->getAssociation()); $fullAssociation = 'o'; @@ -199,14 +225,14 @@ class AdvancedSearchFilter extends AbstractFilter $parent = 'o'; } - $fullAssociation .= '.'.$association; + $fullAssociation .= '.' . $association; $alias = $this->getAlias($fullAssociation); - $queryBuilder->join($parent.'.'.$association, $alias); + $queryBuilder->join($parent . '.' . $association, $alias); } - $this->joins[] = $filter['association']; + $this->joins[] = $filter->getAssociation(); } /** @@ -219,49 +245,49 @@ class AdvancedSearchFilter extends AbstractFilter * * @return \Doctrine\ORM\Query\Expr\Comparison|\Doctrine\ORM\Query\Expr\Func */ - private function getFilterExpression(QueryBuilder $queryBuilder, $filter) + private function getFilterExpression(QueryBuilder $queryBuilder, Filter $filter) { - if ($filter['association'] !== null) { - $alias = $this->getAlias('o.'.$filter['association']).'.'.$filter['property']; + if ($filter->getAssociation() !== null) { + $alias = $this->getAlias('o.' . $filter->getAssociation()) . '.' . $filter->getProperty(); } else { - $alias = 'o.'.$filter['property']; + $alias = 'o.' . $filter->getProperty(); } - if (strtolower($filter['operator']) == self::OPERATOR_IN) { - if (!is_array($filter['value'])) { + if (strtolower($filter->getOperator()) == Filter::OPERATOR_IN) { + if (!is_array($filter->getValue())) { throw new \Exception('Value needs to be an array for the IN operator'); } - return $queryBuilder->expr()->in($alias, $filter['value']); + return $queryBuilder->expr()->in($alias, $filter->getValue()); } else { - $paramName = ':param'.$this->parameterCount; + $paramName = ':param' . $this->parameterCount; $this->parameterCount++; - $queryBuilder->setParameter($paramName, $filter['value']); + $queryBuilder->setParameter($paramName, $filter->getValue()); - switch (strtolower($filter['operator'])) { - case self::OPERATOR_EQUALS: + switch (strtolower($filter->getOperator())) { + case Filter::OPERATOR_EQUALS: return $queryBuilder->expr()->eq($alias, $paramName); break; - case self::OPERATOR_GREATER_THAN: + case Filter::OPERATOR_GREATER_THAN: return $queryBuilder->expr()->gt($alias, $paramName); break; - case self::OPERATOR_GREATER_THAN_EQUALS: + case Filter::OPERATOR_GREATER_THAN_EQUALS: return $queryBuilder->expr()->gte($alias, $paramName); break; - case self::OPERATOR_LESS_THAN: + case Filter::OPERATOR_LESS_THAN: return $queryBuilder->expr()->lt($alias, $paramName); break; - case self::OPERATOR_LESS_THAN_EQUALS: + case Filter::OPERATOR_LESS_THAN_EQUALS: return $queryBuilder->expr()->lte($alias, $paramName); break; - case self::OPERATOR_NOT_EQUALS: + case Filter::OPERATOR_NOT_EQUALS: return $queryBuilder->expr()->neq($alias, $paramName); break; - case self::OPERATOR_LIKE: + case Filter::OPERATOR_LIKE: return $queryBuilder->expr()->like($alias, $paramName); break; default: - throw new \Exception('Unknown filter'); + throw new \Exception('Unknown operator '.$filter->getOperator()); } } } @@ -276,15 +302,15 @@ class AdvancedSearchFilter extends AbstractFilter * * @return \Doctrine\ORM\Query\Expr\Comparison|\Doctrine\ORM\Query\Expr\Func */ - private function applyOrderByExpression(QueryBuilder $queryBuilder, $sorter) + private function applyOrderByExpression(QueryBuilder $queryBuilder, Sorter $sorter) { - if ($sorter['association'] !== null) { - $alias = $this->getAlias('o.'.$sorter['association']).'.'.$sorter['property']; + if ($sorter->getAssociation() !== null) { + $alias = $this->getAlias('o.' . $sorter->getAssociation()) . '.' . $sorter->getProperty(); } else { - $alias = 'o.'.$sorter['property']; + $alias = 'o.' . $sorter->getProperty(); } - return $queryBuilder->addOrderBy($alias, $sorter['direction']); + return $queryBuilder->addOrderBy($alias, $sorter->getDirection()); } /** @@ -333,7 +359,7 @@ class AdvancedSearchFilter extends AbstractFilter private function getAlias($property) { if (!array_key_exists($property, $this->aliases)) { - $this->aliases[$property] = 't'.count($this->aliases); + $this->aliases[$property] = 't' . count($this->aliases); } return $this->aliases[$property]; @@ -350,7 +376,7 @@ class AdvancedSearchFilter extends AbstractFilter */ private function extractJSONFilters($data) { - $filter = []; + $filter = new Filter(); if (property_exists($data, 'property')) { if (strpos($data->property, '.') !== false) { @@ -358,27 +384,36 @@ class AdvancedSearchFilter extends AbstractFilter $property = array_pop($associations); - $filter['association'] = implode('.', $associations); - $filter['property'] = $property; + + $filter->setAssociation(implode('.', $associations)); + $filter->setProperty($property); + } else { + $filter->setAssociation(null); + $filter->setProperty($data->property); + } + } elseif (property_exists($data, "subfilters")) { + if (is_array($data->subfilters)) { + $subfilters = []; + foreach ($data->subfilters as $subfilter) { + $subfilters[] = $this->extractJSONFilters($subfilter); + } + $filter->setSubFilters($subfilters); + return $filter; } else { - $filter['association'] = null; - $filter['property'] = $data->property; + throw new \Exception("The subfilters must be an array of objects"); } } else { throw new \Exception('You need to set the filter property'); } if (property_exists($data, 'operator')) { - if (!in_array(strtolower($data->operator), self::OPERATORS)) { - throw new \Exception(sprintf('Invalid operator %s', $data->operator)); - } - $filter['operator'] = $data->operator; + $filter->setOperator($data->operator); } else { - $filter['operator'] = self::OPERATOR_EQUALS; + $filter->setOperator(Filter::OPERATOR_EQUALS); } if (property_exists($data, 'value')) { - $filter['value'] = $data->value; + $filter->setValue($data->value); } else { throw new \Exception('No value specified'); } @@ -393,11 +428,11 @@ class AdvancedSearchFilter extends AbstractFilter * * @throws \Exception * - * @return array An array containing the property, operator and value keys + * @return Sorter A Sorter object */ private function extractJSONSorters($data) { - $sorter = []; + $sorter = new Sorter(); if ($data->property) { if (strpos($data->property, '.') !== false) { @@ -405,11 +440,11 @@ class AdvancedSearchFilter extends AbstractFilter $property = array_pop($associations); - $sorter['association'] = implode('.', $associations); - $sorter['property'] = $property; + $sorter->setAssociation(implode('.', $associations)); + $sorter->setProperty($property); } else { - $sorter['association'] = null; - $sorter['property'] = $data->property; + $sorter->setAssociation(null); + $sorter->setProperty($data->property); } } else { throw new \Exception('You need to set the filter property'); @@ -418,15 +453,15 @@ class AdvancedSearchFilter extends AbstractFilter if ($data->direction) { switch (strtoupper($data->direction)) { case 'DESC': - $sorter['direction'] = 'DESC'; + $sorter->setDirection("DESC"); break; case 'ASC': default: - $sorter['direction'] = 'ASC'; + $sorter->setDirection("ASC"); break; } } else { - $sorter['direction'] = 'ASC'; + $sorter->setDirection("ASC"); } return $sorter; diff --git a/src/PartKeepr/DoctrineReflectionBundle/Filter/AssociationPropertyInterface.php b/src/PartKeepr/DoctrineReflectionBundle/Filter/AssociationPropertyInterface.php @@ -0,0 +1,11 @@ +<?php +namespace PartKeepr\DoctrineReflectionBundle\Filter; + + +interface AssociationPropertyInterface +{ + public function getProperty (); + public function setProperty($property); + public function getAssociation (); + public function setAssociation($association); +}+ \ No newline at end of file diff --git a/src/PartKeepr/DoctrineReflectionBundle/Filter/AssociationPropertyTrait.php b/src/PartKeepr/DoctrineReflectionBundle/Filter/AssociationPropertyTrait.php @@ -0,0 +1,49 @@ +<?php +namespace PartKeepr\DoctrineReflectionBundle\Filter; + + +trait AssociationPropertyTrait +{ + /** + * @var string + */ + private $property; + /** + * @var string + */ + private $association; + + /** + * @return string + */ + public function getProperty() + { + return $this->property; + } + + /** + * @param string $property + */ + public function setProperty($property) + { + $this->property = $property; + } + + /** + * @return string + */ + public function getAssociation() + { + return $this->association; + } + + /** + * @param string $association + */ + public function setAssociation($association) + { + $this->association = $association; + } + + +}+ \ No newline at end of file diff --git a/src/PartKeepr/DoctrineReflectionBundle/Filter/Filter.php b/src/PartKeepr/DoctrineReflectionBundle/Filter/Filter.php @@ -0,0 +1,147 @@ +<?php +namespace PartKeepr\DoctrineReflectionBundle\Filter; + + +class Filter implements AssociationPropertyInterface +{ + use AssociationPropertyTrait; + + const TYPE_AND = "and"; + const TYPE_OR = "or"; + const OPERATOR_LESS_THAN = '<'; + const OPERATOR_GREATER_THAN = '>'; + const OPERATOR_EQUALS = '='; + const OPERATOR_GREATER_THAN_EQUALS = '>='; + const OPERATOR_LESS_THAN_EQUALS = '>='; + const OPERATOR_NOT_EQUALS = '!='; + const OPERATOR_IN = 'in'; + const OPERATOR_LIKE = 'like'; + + const OPERATORS = [ + self::OPERATOR_LESS_THAN, + self::OPERATOR_GREATER_THAN, + self::OPERATOR_EQUALS, + self::OPERATOR_GREATER_THAN_EQUALS, + self::OPERATOR_LESS_THAN_EQUALS, + self::OPERATOR_NOT_EQUALS, + self::OPERATOR_IN, + self::OPERATOR_LIKE, + ]; + + const TYPES = [ + self::TYPE_AND, + self::TYPE_OR + ]; + + /** + * The type + * @var string + */ + private $type; + /** + * @var string + */ + private $operator; + /** + * @var string + */ + private $value; + /** + * Subfilters + * @var array + */ + private $subFilters; + + + + public function __construct($type = self::TYPE_AND) + { + $this->setType($type); + } + + + + + /** + * @return string + */ + public function getType() + { + return $this->type; + } + + /** + * @param string $type + * @throws \Exception + */ + public function setType($type) + { + if (!in_array($type, self::TYPES)) { + throw new \Exception("Invalid type $type"); + } + $this->type = $type; + } + + /** + * @return string + */ + public function getOperator() + { + return $this->operator; + } + + /** + * @param string $operator + */ + public function setOperator($operator) + { + if (!in_array(strtolower($operator), self::OPERATORS)) { + throw new \Exception("Invalid operator $operator"); + } + $this->operator = strtolower($operator); + } + + /** + * @return string + */ + public function getValue() + { + return $this->value; + } + + /** + * @param string $value + */ + public function setValue($value) + { + $this->value = $value; + } + + /** + * @return array + */ + public function getSubFilters() + { + return $this->subFilters; + } + + /** + * @param array $subFilters + */ + public function setSubFilters($subFilters) + { + $this->subFilters = $subFilters; + } + + public function hasSubFilters () { + return count($this->subFilters) > 0; + } + + + public function validate() + { + + } + + +}+ \ No newline at end of file diff --git a/src/PartKeepr/DoctrineReflectionBundle/Filter/PropertyFilter.php b/src/PartKeepr/DoctrineReflectionBundle/Filter/PropertyFilter.php @@ -0,0 +1,15 @@ +<?php +/** + * Created by PhpStorm. + * User: felicitus + * Date: 7/30/16 + * Time: 5:20 PM + */ + +namespace PartKeepr\DoctrineReflectionBundle\Filter; + + +class PropertyFilter +{ + +}+ \ No newline at end of file diff --git a/src/PartKeepr/DoctrineReflectionBundle/Filter/Sorter.php b/src/PartKeepr/DoctrineReflectionBundle/Filter/Sorter.php @@ -0,0 +1,29 @@ +<?php +namespace PartKeepr\DoctrineReflectionBundle\Filter; + +class Sorter implements AssociationPropertyInterface { + + use AssociationPropertyTrait; + + /** + * @var string + */ + private $direction; + + /** + * @return string + */ + public function getDirection() + { + return $this->direction; + } + + /** + * @param string $direction + */ + public function setDirection($direction) + { + $this->direction = $direction; + } + +}+ \ No newline at end of file diff --git a/src/PartKeepr/DoctrineReflectionBundle/Tests/AdvancedSearchFilterTest.php b/src/PartKeepr/DoctrineReflectionBundle/Tests/AdvancedSearchFilterTest.php @@ -0,0 +1,299 @@ +<?php + +namespace PartKeepr\DoctrineReflectionBundle\Tests; + +use Doctrine\Common\DataFixtures\ProxyReferenceRepository; +use Dunglas\ApiBundle\Api\IriConverter; +use PartKeepr\CoreBundle\Tests\WebTestCase; + +class AdvancedSearchFilterTest extends WebTestCase +{ + /** + * @var ProxyReferenceRepository + */ + protected $fixtures; + + public function setUp() + { + $this->fixtures = $this->loadFixtures( + [ + 'PartKeepr\StorageLocationBundle\DataFixtures\CategoryDataLoader', + 'PartKeepr\StorageLocationBundle\DataFixtures\StorageLocationLoader', + 'PartKeepr\PartBundle\DataFixtures\CategoryDataLoader', + 'PartKeepr\PartBundle\DataFixtures\PartDataLoader', + 'PartKeepr\ManufacturerBundle\Tests\DataFixtures\ManufacturerDataLoader', + 'PartKeepr\DistributorBundle\Tests\DataFixtures\DistributorDataLoader', + ] + )->getReferenceRepository(); + } + + + public function testEqualFilter() + { + $client = static::makeClient(true); + + $filter = array( + array( + "property" => "storageLocation.name", + "operator" => "=", + "value" => "test" + ) + ); + + + $client->request( + 'GET', + "/api/parts?filter=" . json_encode($filter), + [], + [], + ['CONTENT_TYPE' => 'application/json'] + ); + + + $data = json_decode($client->getResponse()->getContent(), true); + + $this->assertArrayHasKey("hydra:member", $data); + $this->assertCount(1, $data["hydra:member"]); + $this->assertArrayHasKey("@id", $data["hydra:member"][0]); + + /** + * @var IriConverter + */ + $iriConverter = $this->getContainer()->get('api.iri_converter'); + + $this->assertEquals($iriConverter->getIriFromItem($this->fixtures->getReference("part.1")), + $data["hydra:member"][0]["@id"]); + } + + public function testEqualFilterSame() + { + $client = static::makeClient(true); + + $filter = array( + array( + "property" => "name", + "operator" => "=", + "value" => "FOOBAR" + ) + ); + + + $client->request( + 'GET', + "/api/parts?filter=" . json_encode($filter), + [], + [], + ['CONTENT_TYPE' => 'application/json'] + ); + + + $data = json_decode($client->getResponse()->getContent(), true); + + $this->assertArrayHasKey("hydra:member", $data); + $this->assertCount(1, $data["hydra:member"]); + $this->assertArrayHasKey("@id", $data["hydra:member"][0]); + + /** + * @var IriConverter + */ + $iriConverter = $this->getContainer()->get('api.iri_converter'); + + $this->assertEquals($iriConverter->getIriFromItem($this->fixtures->getReference("part.1")), + $data["hydra:member"][0]["@id"]); + } + + public function testIDReference() + { + $client = static::makeClient(true); + + /** + * @var IriConverter + */ + $iriConverter = $this->getContainer()->get('api.iri_converter'); + + $filter = array( + array( + "property" => "storageLocation", + "operator" => "=", + "value" => $iriConverter->getIriFromItem($this->fixtures->getReference("storagelocation.first")) + ) + ); + + $client->request( + 'GET', + "/api/parts?filter=" . json_encode($filter), + [], + [], + ['CONTENT_TYPE' => 'application/json'] + ); + + + $data = json_decode($client->getResponse()->getContent(), true); + + $this->assertArrayHasKey("hydra:member", $data); + $this->assertCount(1, $data["hydra:member"]); + } + + public function testIDReferenceArray() + { + $client = static::makeClient(true); + + /** + * @var IriConverter + */ + $iriConverter = $this->getContainer()->get('api.iri_converter'); + + $filter = array( + array( + "property" => "storageLocation", + "operator" => "IN", + "value" => [ + $iriConverter->getIriFromItem($this->fixtures->getReference("storagelocation.first")), + $iriConverter->getIriFromItem($this->fixtures->getReference("storagelocation.second")) + ] + ) + ); + + $client->request( + 'GET', + "/api/parts?filter=" . json_encode($filter), + [], + [], + ['CONTENT_TYPE' => 'application/json'] + ); + + + $data = json_decode($client->getResponse()->getContent(), true); + + $this->assertArrayHasKey("hydra:member", $data); + $this->assertGreaterThan(1, $data["hydra:member"]); + } + + public function testLikeFilter() + { + $client = static::makeClient(true); + + $filter = array( + array( + "property" => "storageLocation.name", + "operator" => "LIKE", + "value" => "%test%" + ) + ); + + + $client->request( + 'GET', + "/api/parts?filter=" . json_encode($filter), + [], + [], + ['CONTENT_TYPE' => 'application/json'] + ); + + + $data = json_decode($client->getResponse()->getContent(), true); + + $this->assertArrayHasKey("hydra:member", $data); + $this->assertCount(2, $data["hydra:member"]); + } + + public function testSorter() + { + $client = static::makeClient(true); + + $order = array( + array( + "property" => "storageLocation.name", + "direction" => "ASC" + ) + ); + + + $client->request( + 'GET', + "/api/parts?order=" . json_encode($order), + [], + [], + ['CONTENT_TYPE' => 'application/json'] + ); + + $data = json_decode($client->getResponse()->getContent(), true); + + $this->assertArrayHasKey("hydra:member", $data); + $this->assertCount(2, $data["hydra:member"]); + } + + public function testOrFilterJoin() + { + $client = static::makeClient(true); + + $filter = array( + array( + "mode" => "OR", + "subfilters" => array( + array( + "property" => "storageLocation.name", + "operator" => "=", + "value" => "test" + ), + array( + "property" => "storageLocation.name", + "operator" => "=", + "value" => "test2" + ) + ) + ) + ); + + + $client->request( + 'GET', + "/api/parts?filter=" . json_encode($filter), + [], + [], + ['CONTENT_TYPE' => 'application/json'] + ); + + $data = json_decode($client->getResponse()->getContent(), true); + + $this->assertArrayHasKey("hydra:member", $data); + $this->assertCount(2, $data["hydra:member"]); + } + + public function testOrFilter() + { + $client = static::makeClient(true); + + $filter = array( + array( + "mode" => "OR", + "subfilters" => array( + array( + "property" => "name", + "operator" => "=", + "value" => "FOOBAR" + ), + array( + "property" => "name", + "operator" => "=", + "value" => "FOOBAR2" + ) + ) + ) + ); + + + $client->request( + 'GET', + "/api/parts?filter=" . json_encode($filter), + [], + [], + ['CONTENT_TYPE' => 'application/json'] + ); + + $data = json_decode($client->getResponse()->getContent(), true); + + $this->assertArrayHasKey("hydra:member", $data); + $this->assertCount(2, $data["hydra:member"]); + } +} diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Editor/EditorGrid.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Editor/EditorGrid.js @@ -135,9 +135,17 @@ Ext.define('PartKeepr.EditorGrid', { }, this) }); + var targetField = this.titleProperty; + + if (this.searchField) { + targetField = this.searchField; + } + + console.log(targetField); + this.searchField = Ext.create("PartKeepr.form.field.SearchField", { store: this.store, - targetField: this.titleProperty + targetField: targetField }); var topToolbarItems = []; diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartsGrid.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Components/Part/PartsGrid.js @@ -58,6 +58,7 @@ Ext.define('PartKeepr.PartsGrid', { autoScroll: false, invalidateScrollerOnRefresh: true, titleProperty: 'name', + searchField: ["name", "description", "comment", "internalPartNumber"], initComponent: function () { diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/Util/Filter.js b/src/PartKeepr/FrontendBundle/Resources/public/js/Util/Filter.js @@ -16,7 +16,7 @@ Ext.define('PartKeepr.util.Filter', { return !Ext.Array.contains(v, this.getCandidateValue(candidate, v)); }; //<debug> - var warn = Ext.util.Filter.isInvalid(config); + var warn = PartKeepr.util.Filter.isInvalid(config); if (warn) { Ext.log.warn(warn); } @@ -28,4 +28,69 @@ Ext.define('PartKeepr.util.Filter', { 'in': 1, 'notin': 1 }, + /** + * Returns this filter's state. + * @return {Object} + */ + getState: function () { + var config = this.getInitialConfig(), + result = {}, + name; + + for (name in config) { + // We only want the instance properties in this case, not inherited ones, + // so we need hasOwnProperty to filter out our class values. + + if (name === "subfilters") { + if (config[name] instanceof Array) { + var tempConfigs = new Array(); + + for (var i=0;i<config[name].length;i++) { + tempConfigs.push(config[name][i].getState()); + } + + result[name] = tempConfigs; + } + } else if (config.hasOwnProperty(name)) { + result[name] = config[name]; + + } + } + + delete result.root; + + if (config["subfilters"] instanceof Array) { + // Do nothing for now + } else { + result.value = this.getValue(); + } + return result; + }, + inheritableStatics: { + /** + * Checks whether the filter will produce a meaningful value. Since filters + * may be used in conjunction with data binding, this is a sanity check to + * check whether the resulting filter will be able to match. + * + * @param {Object} cfg The filter config object + * @return {Boolean} `true` if the filter will produce a valid value + * + * @private + */ + isInvalid: function(cfg) { + return false; + if (!cfg.filterFn) { + // If we don't have a filterFn, we must have a property + if (!cfg.property) { + return 'A Filter requires either a property or a filterFn to be set'; + } + + if (!cfg.hasOwnProperty('value') && !cfg.operator) { + return 'A Filter requires either a property and value, or a filterFn to be set'; + } + + } + return false; + } + } }); diff --git a/src/PartKeepr/FrontendBundle/Resources/public/js/form/field/SearchField.js b/src/PartKeepr/FrontendBundle/Resources/public/js/form/field/SearchField.js @@ -43,15 +43,30 @@ Ext.define('PartKeepr.form.field.SearchField', { } }, - initComponent: function () - { + initComponent: function () { this.callParent(arguments); - this.filter = Ext.create("Ext.util.Filter", { - property: this.targetField, - value: '', - operator: 'like' - }); + if (this.targetField instanceof Array) { + var subFilters = new Array(); + for (var i = 0; i < this.targetField.length; i++) { + subFilters.push(Ext.create("PartKeepr.util.Filter", { + property: this.targetField[i], + value: '', + operator: 'like' + })); + } + + this.filter = Ext.create("PartKeepr.util.Filter", { + type: "OR", + subfilters: subFilters + }); + } else { + this.filter = Ext.create("PartKeepr.util.Filter", { + property: this.targetField, + value: '', + operator: 'like' + }); + } }, /** * Handles special keys used in this field. @@ -59,8 +74,7 @@ Ext.define('PartKeepr.form.field.SearchField', { * Enter: Starts the search * Escape: Removes the search and clears the field contents */ - keyHandler: function (field, e) - { + keyHandler: function (field, e) { switch (e.getKey()) { case e.ENTER: this.startSearch(); @@ -73,8 +87,7 @@ Ext.define('PartKeepr.form.field.SearchField', { /** * Resets the search field to empty and re-triggers the store to load the matching records. */ - resetSearch: function () - { + resetSearch: function () { var me = this, store = me.store; @@ -84,6 +97,12 @@ Ext.define('PartKeepr.form.field.SearchField', { } me.setValue(''); + if (this.filter.subfilters instanceof Array) { + for (var i=0;i<this.filter.subfilters.length;i++) { + this.filter.subfilters[i].setValue(''); + } + } + this.filter.setValue(''); if (me.hasSearch) { @@ -100,8 +119,7 @@ Ext.define('PartKeepr.form.field.SearchField', { /** * Starts the search with the entered value. */ - startSearch: function () - { + startSearch: function () { var me = this, store = me.store, value = me.getValue(), @@ -120,6 +138,13 @@ Ext.define('PartKeepr.form.field.SearchField', { if (this.filter.getValue() === searchValue) { return; } + + if (this.filter.subfilters instanceof Array) { + for (var i=0;i<this.filter.subfilters.length;i++) { + this.filter.subfilters[i].setValue(searchValue); + } + } + this.filter.setValue(searchValue); store.addFilter(this.filter); store.currentPage = 1; @@ -133,8 +158,7 @@ Ext.define('PartKeepr.form.field.SearchField', { * * @param {Ext.data.Store} store The store to set */ - setStore: function (store) - { + setStore: function (store) { this.store = store; } }); diff --git a/src/PartKeepr/PartBundle/DataFixtures/PartDataLoader.php b/src/PartKeepr/PartBundle/DataFixtures/PartDataLoader.php @@ -29,7 +29,7 @@ class PartDataLoader extends AbstractFixture $part2->setName('FOOBAR2'); $category = $this->getReference('partcategory.first'); - $storageLocation = $this->getReference('storagelocation.first'); + $storageLocation = $this->getReference('storagelocation.second'); $part2->setCategory($category); $part2->setStorageLocation($storageLocation); diff --git a/src/PartKeepr/StorageLocationBundle/DataFixtures/StorageLocationLoader.php b/src/PartKeepr/StorageLocationBundle/DataFixtures/StorageLocationLoader.php @@ -14,9 +14,15 @@ class StorageLocationLoader extends AbstractFixture $storageLocation->setName('test'); $storageLocation->setCategory($this->getReference('storagelocationcategory.first')); + $storageLocation2 = new StorageLocation(); + $storageLocation2->setName('test2'); + $storageLocation2->setCategory($this->getReference('storagelocationcategory.second')); + $manager->persist($storageLocation); + $manager->persist($storageLocation2); $manager->flush(); $this->addReference('storagelocation.first', $storageLocation); + $this->addReference('storagelocation.second', $storageLocation2); } }