mirror of
https://github.com/Part-DB/Part-DB-server.git
synced 2025-06-21 09:35:49 +02:00
Use natural sorting for trees and others repository functions
This commit is contained in:
parent
9db822eabd
commit
8bb8118d9f
13 changed files with 71 additions and 44 deletions
|
@ -25,7 +25,7 @@ final class Version20240606203053 extends AbstractMultiPlatformMigration impleme
|
||||||
public function postgreSQLUp(Schema $schema): void
|
public function postgreSQLUp(Schema $schema): void
|
||||||
{
|
{
|
||||||
//Create a collation for natural sorting
|
//Create a collation for natural sorting
|
||||||
$this->addSql("CREATE COLLATION numeric (provider = icu, locale = 'en@colNumeric=yes');");
|
$this->addSql("CREATE COLLATION numeric (provider = icu, locale = 'en-u-kn-true');");
|
||||||
|
|
||||||
$this->addSql('CREATE TABLE api_tokens (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, valid_until TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, token VARCHAR(68) NOT NULL, level SMALLINT NOT NULL, last_time_used TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, user_id INT DEFAULT NULL, PRIMARY KEY(id))');
|
$this->addSql('CREATE TABLE api_tokens (id INT GENERATED BY DEFAULT AS IDENTITY NOT NULL, name VARCHAR(255) NOT NULL, valid_until TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, token VARCHAR(68) NOT NULL, level SMALLINT NOT NULL, last_time_used TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT NULL, last_modified TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, datetime_added TIMESTAMP(0) WITHOUT TIME ZONE DEFAULT CURRENT_TIMESTAMP NOT NULL, user_id INT DEFAULT NULL, PRIMARY KEY(id))');
|
||||||
$this->addSql('CREATE UNIQUE INDEX UNIQ_2CAD560E5F37A13B ON api_tokens (token)');
|
$this->addSql('CREATE UNIQUE INDEX UNIQ_2CAD560E5F37A13B ON api_tokens (token)');
|
||||||
|
|
|
@ -25,6 +25,7 @@ namespace App\Doctrine\Functions;
|
||||||
|
|
||||||
use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver;
|
use Doctrine\DBAL\Driver\AbstractPostgreSQLDriver;
|
||||||
use Doctrine\DBAL\Platforms\MariaDBPlatform;
|
use Doctrine\DBAL\Platforms\MariaDBPlatform;
|
||||||
|
use Doctrine\DBAL\Platforms\PostgreSQLPlatform;
|
||||||
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
|
use Doctrine\ORM\Query\AST\Functions\FunctionNode;
|
||||||
use Doctrine\ORM\Query\AST\Node;
|
use Doctrine\ORM\Query\AST\Node;
|
||||||
use Doctrine\ORM\Query\Parser;
|
use Doctrine\ORM\Query\Parser;
|
||||||
|
@ -33,7 +34,7 @@ use Doctrine\ORM\Query\TokenType;
|
||||||
|
|
||||||
class Natsort extends FunctionNode
|
class Natsort extends FunctionNode
|
||||||
{
|
{
|
||||||
private Node $field;
|
private ?Node $field = null;
|
||||||
|
|
||||||
public function parse(Parser $parser): void
|
public function parse(Parser $parser): void
|
||||||
{
|
{
|
||||||
|
@ -47,15 +48,17 @@ class Natsort extends FunctionNode
|
||||||
|
|
||||||
public function getSql(SqlWalker $sqlWalker): string
|
public function getSql(SqlWalker $sqlWalker): string
|
||||||
{
|
{
|
||||||
|
assert($this->field !== null, 'Field is not set');
|
||||||
|
|
||||||
$platform = $sqlWalker->getConnection()->getDatabasePlatform();
|
$platform = $sqlWalker->getConnection()->getDatabasePlatform();
|
||||||
|
|
||||||
if ($platform instanceof AbstractPostgreSQLDriver) {
|
if ($platform instanceof PostgreSQLPlatform) {
|
||||||
return $this->field->dispatch($sqlWalker) . ' COLLATE numeric';
|
return $this->field->dispatch($sqlWalker) . ' COLLATE numeric';
|
||||||
}
|
}
|
||||||
|
|
||||||
if ($platform instanceof MariaDBPlatform && $sqlWalker->getConnection()->getServerVersion()) {
|
/*if ($platform instanceof MariaDBPlatform && $sqlWalker->getConnection()->getServerVersion()) {
|
||||||
|
|
||||||
}
|
}*/
|
||||||
|
|
||||||
//For every other platform, return the field as is
|
//For every other platform, return the field as is
|
||||||
return $this->field->dispatch($sqlWalker);
|
return $this->field->dispatch($sqlWalker);
|
||||||
|
|
|
@ -30,11 +30,11 @@ interface PartsContainingRepositoryInterface
|
||||||
* Returns all parts associated with this element.
|
* Returns all parts associated with this element.
|
||||||
*
|
*
|
||||||
* @param object $element the element for which the parts should be determined
|
* @param object $element the element for which the parts should be determined
|
||||||
* @param array $order_by The order of the parts. Format ['name' => 'ASC']
|
* @param string $nameOrderDirection the direction in which the parts should be ordered by name, either ASC or DESC
|
||||||
*
|
*
|
||||||
* @return Part[]
|
* @return Part[]
|
||||||
*/
|
*/
|
||||||
public function getParts(object $element, array $order_by = ['name' => 'ASC']): array;
|
public function getParts(object $element, string $nameOrderDirection = "ASC"): array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the count of the parts associated with this element.
|
* Gets the count of the parts associated with this element.
|
||||||
|
|
|
@ -40,11 +40,11 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo
|
||||||
* Returns all parts associated with this element.
|
* Returns all parts associated with this element.
|
||||||
*
|
*
|
||||||
* @param object $element the element for which the parts should be determined
|
* @param object $element the element for which the parts should be determined
|
||||||
* @param array $order_by The order of the parts. Format ['name' => 'ASC']
|
* @param string $nameOrderDirection the direction in which the parts should be ordered by name, either ASC or DESC
|
||||||
*
|
*
|
||||||
* @return Part[]
|
* @return Part[]
|
||||||
*/
|
*/
|
||||||
abstract public function getParts(object $element, array $order_by = ['name' => 'ASC']): array;
|
abstract public function getParts(object $element, string $nameOrderDirection = "ASC"): array;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets the count of the parts associated with this element.
|
* Gets the count of the parts associated with this element.
|
||||||
|
@ -113,7 +113,7 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo
|
||||||
return $parts;
|
return $parts;
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getPartsByField(object $element, array $order_by, string $field_name): array
|
protected function getPartsByField(object $element, string $nameOrderDirection, string $field_name): array
|
||||||
{
|
{
|
||||||
if (!$element instanceof AbstractPartsContainingDBElement) {
|
if (!$element instanceof AbstractPartsContainingDBElement) {
|
||||||
throw new InvalidArgumentException('$element must be an instance of AbstractPartContainingDBElement!');
|
throw new InvalidArgumentException('$element must be an instance of AbstractPartContainingDBElement!');
|
||||||
|
@ -121,7 +121,14 @@ abstract class AbstractPartsContainingRepository extends StructuralDBElementRepo
|
||||||
|
|
||||||
$repo = $this->getEntityManager()->getRepository(Part::class);
|
$repo = $this->getEntityManager()->getRepository(Part::class);
|
||||||
|
|
||||||
return $repo->findBy([$field_name => $element], $order_by);
|
//Build a query builder to get the parts with a custom order by
|
||||||
|
|
||||||
|
$qb = $repo->createQueryBuilder('part')
|
||||||
|
->where('part.'.$field_name.' = :element')
|
||||||
|
->setParameter('element', $element)
|
||||||
|
->orderBy('NATSORT(part.name)', $nameOrderDirection);
|
||||||
|
|
||||||
|
return $qb->getQuery()->getResult();
|
||||||
}
|
}
|
||||||
|
|
||||||
protected function getPartsCountByField(object $element, string $field_name): int
|
protected function getPartsCountByField(object $element, string $field_name): int
|
||||||
|
|
|
@ -42,7 +42,7 @@ class NamedDBElementRepository extends DBElementRepository
|
||||||
{
|
{
|
||||||
$result = [];
|
$result = [];
|
||||||
|
|
||||||
$entities = $this->findBy([], ['name' => 'ASC']);
|
$entities = $this->getFlatList();
|
||||||
foreach ($entities as $entity) {
|
foreach ($entities as $entity) {
|
||||||
/** @var AbstractNamedDBElement $entity */
|
/** @var AbstractNamedDBElement $entity */
|
||||||
$node = new TreeViewNode($entity->getName(), null, null);
|
$node = new TreeViewNode($entity->getName(), null, null);
|
||||||
|
@ -65,13 +65,17 @@ class NamedDBElementRepository extends DBElementRepository
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns a flattened list of all nodes.
|
* Returns a flattened list of all nodes, sorted by name in natural order.
|
||||||
* @return AbstractNamedDBElement[]
|
* @return AbstractNamedDBElement[]
|
||||||
* @phpstan-return array<int, AbstractNamedDBElement>
|
* @phpstan-return array<int, AbstractNamedDBElement>
|
||||||
*/
|
*/
|
||||||
public function getFlatList(): array
|
public function getFlatList(): array
|
||||||
{
|
{
|
||||||
//All nodes are sorted by name
|
$qb = $this->createQueryBuilder('e');
|
||||||
return $this->findBy([], ['name' => 'ASC']);
|
$q = $qb->select('e')
|
||||||
|
->orderBy('NATSORT(e.name)', 'ASC')
|
||||||
|
->getQuery();
|
||||||
|
|
||||||
|
return $q->getResult();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -90,7 +90,7 @@ class PartRepository extends NamedDBElementRepository
|
||||||
$qb->setParameter('query', '%'.$query.'%');
|
$qb->setParameter('query', '%'.$query.'%');
|
||||||
|
|
||||||
$qb->setMaxResults($max_limits);
|
$qb->setMaxResults($max_limits);
|
||||||
$qb->orderBy('part.name', 'ASC');
|
$qb->orderBy('NATSORT(part.name)', 'ASC');
|
||||||
|
|
||||||
return $qb->getQuery()->getResult();
|
return $qb->getQuery()->getResult();
|
||||||
}
|
}
|
||||||
|
|
|
@ -28,13 +28,13 @@ use InvalidArgumentException;
|
||||||
|
|
||||||
class CategoryRepository extends AbstractPartsContainingRepository
|
class CategoryRepository extends AbstractPartsContainingRepository
|
||||||
{
|
{
|
||||||
public function getParts(object $element, array $order_by = ['name' => 'ASC']): array
|
public function getParts(object $element, string $nameOrderDirection = "ASC"): array
|
||||||
{
|
{
|
||||||
if (!$element instanceof Category) {
|
if (!$element instanceof Category) {
|
||||||
throw new InvalidArgumentException('$element must be an Category!');
|
throw new InvalidArgumentException('$element must be an Category!');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->getPartsByField($element, $order_by, 'category');
|
return $this->getPartsByField($element, $nameOrderDirection, 'category');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPartsCount(object $element): int
|
public function getPartsCount(object $element): int
|
||||||
|
|
|
@ -28,13 +28,13 @@ use InvalidArgumentException;
|
||||||
|
|
||||||
class FootprintRepository extends AbstractPartsContainingRepository
|
class FootprintRepository extends AbstractPartsContainingRepository
|
||||||
{
|
{
|
||||||
public function getParts(object $element, array $order_by = ['name' => 'ASC']): array
|
public function getParts(object $element, string $nameOrderDirection = "ASC"): array
|
||||||
{
|
{
|
||||||
if (!$element instanceof Footprint) {
|
if (!$element instanceof Footprint) {
|
||||||
throw new InvalidArgumentException('$element must be an Footprint!');
|
throw new InvalidArgumentException('$element must be an Footprint!');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->getPartsByField($element, $order_by, 'footprint');
|
return $this->getPartsByField($element, $nameOrderDirection, 'footprint');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPartsCount(object $element): int
|
public function getPartsCount(object $element): int
|
||||||
|
|
|
@ -28,13 +28,13 @@ use InvalidArgumentException;
|
||||||
|
|
||||||
class ManufacturerRepository extends AbstractPartsContainingRepository
|
class ManufacturerRepository extends AbstractPartsContainingRepository
|
||||||
{
|
{
|
||||||
public function getParts(object $element, array $order_by = ['name' => 'ASC']): array
|
public function getParts(object $element, string $nameOrderDirection = "ASC"): array
|
||||||
{
|
{
|
||||||
if (!$element instanceof Manufacturer) {
|
if (!$element instanceof Manufacturer) {
|
||||||
throw new InvalidArgumentException('$element must be an Manufacturer!');
|
throw new InvalidArgumentException('$element must be an Manufacturer!');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->getPartsByField($element, $order_by, 'manufacturer');
|
return $this->getPartsByField($element, $nameOrderDirection, 'manufacturer');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPartsCount(object $element): int
|
public function getPartsCount(object $element): int
|
||||||
|
|
|
@ -28,13 +28,13 @@ use InvalidArgumentException;
|
||||||
|
|
||||||
class MeasurementUnitRepository extends AbstractPartsContainingRepository
|
class MeasurementUnitRepository extends AbstractPartsContainingRepository
|
||||||
{
|
{
|
||||||
public function getParts(object $element, array $order_by = ['name' => 'ASC']): array
|
public function getParts(object $element, string $nameOrderDirection = "ASC"): array
|
||||||
{
|
{
|
||||||
if (!$element instanceof MeasurementUnit) {
|
if (!$element instanceof MeasurementUnit) {
|
||||||
throw new InvalidArgumentException('$element must be an MeasurementUnit!');
|
throw new InvalidArgumentException('$element must be an MeasurementUnit!');
|
||||||
}
|
}
|
||||||
|
|
||||||
return $this->getPartsByField($element, $order_by, 'partUnit');
|
return $this->getPartsByField($element, $nameOrderDirection, 'partUnit');
|
||||||
}
|
}
|
||||||
|
|
||||||
public function getPartsCount(object $element): int
|
public function getPartsCount(object $element): int
|
||||||
|
|
|
@ -30,12 +30,7 @@ use InvalidArgumentException;
|
||||||
|
|
||||||
class StorelocationRepository extends AbstractPartsContainingRepository
|
class StorelocationRepository extends AbstractPartsContainingRepository
|
||||||
{
|
{
|
||||||
/**
|
public function getParts(object $element, string $nameOrderDirection = "ASC"): array
|
||||||
* @param object $element
|
|
||||||
* @param array $order_by
|
|
||||||
* @return array
|
|
||||||
*/
|
|
||||||
public function getParts(object $element, array $order_by = ['name' => 'ASC']): array
|
|
||||||
{
|
{
|
||||||
if (!$element instanceof StorageLocation) {
|
if (!$element instanceof StorageLocation) {
|
||||||
throw new InvalidArgumentException('$element must be an Storelocation!');
|
throw new InvalidArgumentException('$element must be an Storelocation!');
|
||||||
|
@ -47,11 +42,9 @@ class StorelocationRepository extends AbstractPartsContainingRepository
|
||||||
->from(Part::class, 'part')
|
->from(Part::class, 'part')
|
||||||
->leftJoin('part.partLots', 'lots')
|
->leftJoin('part.partLots', 'lots')
|
||||||
->where('lots.storage_location = ?1')
|
->where('lots.storage_location = ?1')
|
||||||
->setParameter(1, $element);
|
->setParameter(1, $element)
|
||||||
|
->orderBy('NATSORT(part.name)', $nameOrderDirection)
|
||||||
foreach ($order_by as $field => $order) {
|
;
|
||||||
$qb->addOrderBy('part.'.$field, $order);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $qb->getQuery()->getResult();
|
return $qb->getQuery()->getResult();
|
||||||
}
|
}
|
||||||
|
|
|
@ -30,7 +30,7 @@ use InvalidArgumentException;
|
||||||
|
|
||||||
class SupplierRepository extends AbstractPartsContainingRepository
|
class SupplierRepository extends AbstractPartsContainingRepository
|
||||||
{
|
{
|
||||||
public function getParts(object $element, array $order_by = ['name' => 'ASC']): array
|
public function getParts(object $element, string $nameOrderDirection = "ASC"): array
|
||||||
{
|
{
|
||||||
if (!$element instanceof Supplier) {
|
if (!$element instanceof Supplier) {
|
||||||
throw new InvalidArgumentException('$element must be an Supplier!');
|
throw new InvalidArgumentException('$element must be an Supplier!');
|
||||||
|
@ -42,11 +42,9 @@ class SupplierRepository extends AbstractPartsContainingRepository
|
||||||
->from(Part::class, 'part')
|
->from(Part::class, 'part')
|
||||||
->leftJoin('part.orderdetails', 'orderdetail')
|
->leftJoin('part.orderdetails', 'orderdetail')
|
||||||
->where('orderdetail.supplier = ?1')
|
->where('orderdetail.supplier = ?1')
|
||||||
->setParameter(1, $element);
|
->setParameter(1, $element)
|
||||||
|
->orderBy('NATSORT(part.name)', $nameOrderDirection)
|
||||||
foreach ($order_by as $field => $order) {
|
;
|
||||||
$qb->addOrderBy('part.'.$field, $order);
|
|
||||||
}
|
|
||||||
|
|
||||||
return $qb->getQuery()->getResult();
|
return $qb->getQuery()->getResult();
|
||||||
}
|
}
|
||||||
|
|
|
@ -40,6 +40,28 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit
|
||||||
*/
|
*/
|
||||||
private array $new_entity_cache = [];
|
private array $new_entity_cache = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Finds all nodes for the given parent node, ordered by name in a natural sort way
|
||||||
|
* @param AbstractStructuralDBElement|null $parent
|
||||||
|
* @param string $nameOrdering The ordering of the names. Either ASC or DESC
|
||||||
|
* @return array
|
||||||
|
*/
|
||||||
|
public function findNodesForParent(?AbstractStructuralDBElement $parent, string $nameOrdering = "ASC"): array
|
||||||
|
{
|
||||||
|
$qb = $this->createQueryBuilder('e');
|
||||||
|
$qb->select('e')
|
||||||
|
->orderBy('NATSORT(e.name)', $nameOrdering);
|
||||||
|
|
||||||
|
if ($parent) {
|
||||||
|
$qb->where('e.parent = :parent')
|
||||||
|
->setParameter('parent', $parent);
|
||||||
|
} else {
|
||||||
|
$qb->where('e.parent IS NULL');
|
||||||
|
}
|
||||||
|
//@phpstan-ignore-next-line [parent is only defined by the sub classes]
|
||||||
|
return $qb->getQuery()->getResult();
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds all nodes without a parent node. They are our root nodes.
|
* Finds all nodes without a parent node. They are our root nodes.
|
||||||
*
|
*
|
||||||
|
@ -47,7 +69,7 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit
|
||||||
*/
|
*/
|
||||||
public function findRootNodes(): array
|
public function findRootNodes(): array
|
||||||
{
|
{
|
||||||
return $this->findBy(['parent' => null], ['name' => 'ASC']);
|
return $this->findNodesForParent(null);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -63,7 +85,7 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit
|
||||||
{
|
{
|
||||||
$result = [];
|
$result = [];
|
||||||
|
|
||||||
$entities = $this->findBy(['parent' => $parent], ['name' => 'ASC']);
|
$entities = $this->findNodesForParent($parent);
|
||||||
foreach ($entities as $entity) {
|
foreach ($entities as $entity) {
|
||||||
/** @var AbstractStructuralDBElement $entity */
|
/** @var AbstractStructuralDBElement $entity */
|
||||||
//Make a recursive call to find all children nodes
|
//Make a recursive call to find all children nodes
|
||||||
|
@ -89,7 +111,7 @@ class StructuralDBElementRepository extends AttachmentContainingDBElementReposit
|
||||||
{
|
{
|
||||||
$result = [];
|
$result = [];
|
||||||
|
|
||||||
$entities = $this->findBy(['parent' => $parent], ['name' => 'ASC']);
|
$entities = $this->findNodesForParent($parent);
|
||||||
|
|
||||||
$elementIterator = new StructuralDBElementIterator($entities);
|
$elementIterator = new StructuralDBElementIterator($entities);
|
||||||
$recursiveIterator = new RecursiveIteratorIterator($elementIterator, RecursiveIteratorIterator::SELF_FIRST);
|
$recursiveIterator = new RecursiveIteratorIterator($elementIterator, RecursiveIteratorIterator::SELF_FIRST);
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue