Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions config/data_index_filters.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,9 @@ services:
Pimcore\Bundle\StudioBackendBundle\DataIndex\Filter\DataObject\ClassIdsFilter:
tags: [ 'pimcore.studio_backend.search_index.data_object.filter' ]

Pimcore\Bundle\StudioBackendBundle\DataIndex\Filter\DataObject\RelationFilter:
tags: [ 'pimcore.studio_backend.search_index.data_object.filter' ]

Pimcore\Bundle\StudioBackendBundle\DataIndex\Filter\DataObject\Classificationstore\StringFilter:
tags: [ 'pimcore.studio_backend.search_index.data_object.filter' ]

Expand Down
20 changes: 20 additions & 0 deletions doc/03_Grid.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ Available filters are:
| classificationstore.boolean | array | `true`, `false` or `null` | true |
| classificationstore.number | object | `from`, `to`, `is`, `setting` | true |
| crm.consent | array | `true` or `false` | true |
| dataobject.relation | object | `type`, `ids` | true |


### Examples:
Expand Down Expand Up @@ -140,6 +141,25 @@ The filter value of a Classification Store looks a bit difrent. All Filter need
...
```

Filter by DataObject Relation:
The `dataobject.relation` filter allows filtering by relation fields. The filter value must contain a `type` (the element type: `asset`, `object`, or `document`) and an `ids` array with the relation IDs to filter by.

To filter by this structure:
```json
...
"columnFilters" [
{
"key": "bodyStyle",
"type": "dataobject.relation",
"filterValue": {
"type": "object",
"ids": [6]
}
}
]
...
```

## Advanced Columns
Advanced columns are a special type of column that can be used to display data in a more advanced way. There are a few types of data sources for advanced columns:
- `simpleField` - a simple field in the object
Expand Down
81 changes: 81 additions & 0 deletions src/DataIndex/Filter/DataObject/RelationFilter.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
<?php
declare(strict_types=1);

/**
* This source file is available under the terms of the
* Pimcore Open Core License (POCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
* @license Pimcore Open Core License (POCL)
*/

namespace Pimcore\Bundle\StudioBackendBundle\DataIndex\Filter\DataObject;

use Pimcore\Bundle\StudioBackendBundle\DataIndex\Filter\FilterInterface;
use Pimcore\Bundle\StudioBackendBundle\DataIndex\Query\DataObjectQueryInterface;
use Pimcore\Bundle\StudioBackendBundle\DataIndex\Query\QueryInterface;
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidArgumentException;
use Pimcore\Bundle\StudioBackendBundle\Grid\Column\ColumnType;
use Pimcore\Bundle\StudioBackendBundle\MappedParameter\Filter\ColumnFilter;
use Pimcore\Bundle\StudioBackendBundle\MappedParameter\Filter\ColumnFiltersParameterInterface;

use function in_array;
use function is_array;

/**
* @internal
*/
final class RelationFilter implements FilterInterface
{
private const ALLOWED_TYPES = ['asset', 'object', 'document'];

/**
* @throws InvalidArgumentException
*/
public function apply(mixed $parameters, QueryInterface $query): QueryInterface
{
if (
!$parameters instanceof ColumnFiltersParameterInterface ||
!$query instanceof DataObjectQueryInterface
) {
return $query;
}

foreach ($parameters->getColumnFilterByType(ColumnType::DATAOBJECT_RELATION->value) as $column) {
$query = $this->applyRelationFilter($column, $query);
}

return $query;
}

/**
* @throws InvalidArgumentException
*/
private function applyRelationFilter(ColumnFilter $column, QueryInterface $query): QueryInterface
{
$filterValue = $column->getFilterValue();

if (!is_array($filterValue)) {
throw new InvalidArgumentException('Value for relation filter must be an array');
}

$type = $filterValue['type'] ?? null;
$ids = $filterValue['ids'] ?? null;

if ($type === null || !in_array($type, self::ALLOWED_TYPES, true)) {
throw new InvalidArgumentException(
'Value for relation filter must contain a valid type (asset, object, document)'
);
}

if (!is_array($ids) || $ids === []) {
throw new InvalidArgumentException('Value for relation filter must contain a non-empty ids array');
}

$fieldKey = $column->getKey() . '.' . $type;

return $query->filterMultiSelect($fieldKey, $ids);
}
}
1 change: 1 addition & 0 deletions src/Grid/Column/ColumnType.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,5 @@ enum ColumnType: string
case CLASSIFICATION_STORE_SELECT = 'classificationstore.select';
case CLASSIFICATION_STORE_BOOLEAN = 'classificationstore.boolean';
case CRM_CONSENT = 'crm.consent';
case DATAOBJECT_RELATION = 'dataobject.relation';
}
230 changes: 230 additions & 0 deletions tests/Unit/DataIndex/Filter/DataObject/RelationFilterTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,230 @@
<?php
declare(strict_types=1);

/**
* This source file is available under the terms of the
* Pimcore Open Core License (POCL)
* Full copyright and license information is available in
* LICENSE.md which is distributed with this source code.
*
* @copyright Copyright (c) Pimcore GmbH (https://www.pimcore.com)
* @license Pimcore Open Core License (POCL)
*/

namespace Pimcore\Bundle\StudioBackendBundle\Tests\Unit\DataIndex\Filter\DataObject;

use Codeception\Stub\Expected;
use Codeception\Test\Unit;
use Pimcore\Bundle\StudioBackendBundle\DataIndex\Filter\DataObject\RelationFilter;
use Pimcore\Bundle\StudioBackendBundle\DataIndex\Query\AssetQueryInterface;
use Pimcore\Bundle\StudioBackendBundle\DataIndex\Query\DataObjectQueryInterface;
use Pimcore\Bundle\StudioBackendBundle\Exception\Api\InvalidArgumentException;
use Pimcore\Bundle\StudioBackendBundle\Grid\Column\ColumnType;
use Pimcore\Bundle\StudioBackendBundle\MappedParameter\Filter\ColumnFilter;
use Pimcore\Bundle\StudioBackendBundle\MappedParameter\Filter\ColumnFiltersParameterInterface;

/**
* @internal
*/
final class RelationFilterTest extends Unit
{
public function testRelationFilterReturnsQueryWhenParametersNotColumnFiltersParameterInterface(): void
{
$filter = new RelationFilter();
$queryMock = $this->makeEmpty(DataObjectQueryInterface::class);

$result = $filter->apply('invalid', $queryMock);

$this->assertSame($queryMock, $result);
}

public function testRelationFilterReturnsQueryWhenQueryNotDataObjectQueryInterface(): void
{
$filter = new RelationFilter();
$parameterMock = $this->makeEmpty(ColumnFiltersParameterInterface::class);
$queryMock = $this->makeEmpty(AssetQueryInterface::class);

$result = $filter->apply($parameterMock, $queryMock);

$this->assertSame($queryMock, $result);
}

public function testRelationFilterThrowsExceptionWhenFilterValueNotArray(): void
{
$filter = new RelationFilter();
$parameterMock = $this->makeEmpty(ColumnFiltersParameterInterface::class, [
'getColumnFilterByType' => function () {
return [
new ColumnFilter('bodyStyle', ColumnType::DATAOBJECT_RELATION->value, 'invalid'),
];
},
]);
$queryMock = $this->makeEmpty(DataObjectQueryInterface::class);

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Value for relation filter must be an array');

$filter->apply($parameterMock, $queryMock);
}

public function testRelationFilterThrowsExceptionWhenTypeIsMissing(): void
{
$filter = new RelationFilter();
$parameterMock = $this->makeEmpty(ColumnFiltersParameterInterface::class, [
'getColumnFilterByType' => function () {
return [
new ColumnFilter('bodyStyle', ColumnType::DATAOBJECT_RELATION->value, ['ids' => [1, 2]]),
];
},
]);
$queryMock = $this->makeEmpty(DataObjectQueryInterface::class);

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Value for relation filter must contain a valid type (asset, object, document)');

$filter->apply($parameterMock, $queryMock);
}

public function testRelationFilterThrowsExceptionWhenTypeIsInvalid(): void
{
$filter = new RelationFilter();
$parameterMock = $this->makeEmpty(ColumnFiltersParameterInterface::class, [
'getColumnFilterByType' => function () {
return [
new ColumnFilter(
'bodyStyle',
ColumnType::DATAOBJECT_RELATION->value,
['type' => 'invalid', 'ids' => [1, 2]]
),
];
},
]);
$queryMock = $this->makeEmpty(DataObjectQueryInterface::class);

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Value for relation filter must contain a valid type (asset, object, document)');

$filter->apply($parameterMock, $queryMock);
}

public function testRelationFilterThrowsExceptionWhenIdsIsMissing(): void
{
$filter = new RelationFilter();
$parameterMock = $this->makeEmpty(ColumnFiltersParameterInterface::class, [
'getColumnFilterByType' => function () {
return [
new ColumnFilter('bodyStyle', ColumnType::DATAOBJECT_RELATION->value, ['type' => 'object']),
];
},
]);
$queryMock = $this->makeEmpty(DataObjectQueryInterface::class);

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Value for relation filter must contain a non-empty ids array');

$filter->apply($parameterMock, $queryMock);
}

public function testRelationFilterThrowsExceptionWhenIdsIsEmpty(): void
{
$filter = new RelationFilter();
$parameterMock = $this->makeEmpty(ColumnFiltersParameterInterface::class, [
'getColumnFilterByType' => function () {
return [
new ColumnFilter(
'bodyStyle',
ColumnType::DATAOBJECT_RELATION->value,
['type' => 'object', 'ids' => []]
),
];
},
]);
$queryMock = $this->makeEmpty(DataObjectQueryInterface::class);

$this->expectException(InvalidArgumentException::class);
$this->expectExceptionMessage('Value for relation filter must contain a non-empty ids array');

$filter->apply($parameterMock, $queryMock);
}

public function testRelationFilterAppliesFilterWithObjectType(): void
{
$filter = new RelationFilter();
$parameterMock = $this->makeEmpty(ColumnFiltersParameterInterface::class, [
'getColumnFilterByType' => function () {
return [
new ColumnFilter(
'bodyStyle',
ColumnType::DATAOBJECT_RELATION->value,
['type' => 'object', 'ids' => [6]]
),
];
},
]);

$queryMock = $this->makeEmpty(DataObjectQueryInterface::class, [
'filterMultiSelect' => Expected::once(function ($fieldKey, $ids) {
$this->assertSame('bodyStyle.object', $fieldKey);
$this->assertSame([6], $ids);

return $this->makeEmpty(DataObjectQueryInterface::class);
}),
]);

$filter->apply($parameterMock, $queryMock);
}

public function testRelationFilterAppliesFilterWithAssetType(): void
{
$filter = new RelationFilter();
$parameterMock = $this->makeEmpty(ColumnFiltersParameterInterface::class, [
'getColumnFilterByType' => function () {
return [
new ColumnFilter(
'images',
ColumnType::DATAOBJECT_RELATION->value,
['type' => 'asset', 'ids' => [10, 20]]
),
];
},
]);

$queryMock = $this->makeEmpty(DataObjectQueryInterface::class, [
'filterMultiSelect' => Expected::once(function ($fieldKey, $ids) {
$this->assertSame('images.asset', $fieldKey);
$this->assertSame([10, 20], $ids);

return $this->makeEmpty(DataObjectQueryInterface::class);
}),
]);

$filter->apply($parameterMock, $queryMock);
}

public function testRelationFilterAppliesFilterWithDocumentType(): void
{
$filter = new RelationFilter();
$parameterMock = $this->makeEmpty(ColumnFiltersParameterInterface::class, [
'getColumnFilterByType' => function () {
return [
new ColumnFilter(
'relatedDocuments',
ColumnType::DATAOBJECT_RELATION->value,
['type' => 'document', 'ids' => [5, 15, 25]]
),
];
},
]);

$queryMock = $this->makeEmpty(DataObjectQueryInterface::class, [
'filterMultiSelect' => Expected::once(function ($fieldKey, $ids) {
$this->assertSame('relatedDocuments.document', $fieldKey);
$this->assertSame([5, 15, 25], $ids);

return $this->makeEmpty(DataObjectQueryInterface::class);
}),
]);

$filter->apply($parameterMock, $queryMock);
}
}
Loading