diff --git a/lib/Constants/FilterOperator.php b/lib/Constants/FilterOperator.php index 3cd28ae18a..5acaaaeccb 100644 --- a/lib/Constants/FilterOperator.php +++ b/lib/Constants/FilterOperator.php @@ -12,6 +12,7 @@ enum FilterOperator: string { case BEGINS_WITH = 'begins-with'; case ENDS_WITH = 'ends-with'; case CONTAINS = 'contains'; + case CONTAINS_ITEM = 'contains-item'; case DOES_NOT_CONTAIN = 'does-not-contain'; case IS_EQUAL = 'is-equal'; case IS_NOT_EQUAL = 'is-not-equal'; diff --git a/lib/Db/Row2Mapper.php b/lib/Db/Row2Mapper.php index b0cfa89d9a..c76430a102 100644 --- a/lib/Db/Row2Mapper.php +++ b/lib/Db/Row2Mapper.php @@ -478,6 +478,36 @@ private function getFilterExpression(IQueryBuilder $qb, Column $column, string $ } $filterExpression = $qb->expr()->iLike('value', $qb->createNamedParameter('%' . $this->db->escapeLikeParameter($value) . '%', $paramType)); break; + case 'contains-item': + $filterItems = $value; + if (!is_array($filterItems)) { + $filterItems = json_decode((string)$filterItems, true) ?? null; + if (is_string($filterItems)) { + $filterItems = json_decode($filterItems, true) ?? null; + } + if (!is_array($filterItems) || $filterItems === []) { + $filterItems = [(string)$value]; + } + } + $filterExpressions = []; + $includeDefault = false; + foreach ($filterItems as $filterItem) { + if ($column->getType() === 'selection' && $column->getSubtype() === 'multi') { + $filterItem = str_replace(['"', '\''], '', (string)$filterItem); + $filterExpressions[] = $qb2->expr()->orX( + $qb->expr()->iLike('value', $qb->createNamedParameter('[' . $this->db->escapeLikeParameter($filterItem) . ']')), + $qb->expr()->iLike('value', $qb->createNamedParameter('[' . $this->db->escapeLikeParameter($filterItem) . ',%')), + $qb->expr()->iLike('value', $qb->createNamedParameter('%,' . $this->db->escapeLikeParameter($filterItem) . ']%')), + $qb->expr()->iLike('value', $qb->createNamedParameter('%,' . $this->db->escapeLikeParameter($filterItem) . ',%')) + ); + $includeDefault = $includeDefault || str_contains((string)($defaultValue ?? ''), $filterItem); + } else { + $filterExpressions[] = $qb->expr()->eq('value', $qb->createNamedParameter($filterItem, $paramType)); + $includeDefault = $includeDefault || ((string)($defaultValue ?? '') === (string)$filterItem); + } + } + $filterExpression = $qb2->expr()->orX(...$filterExpressions); + break; case 'does-not-contain': if (is_array($value) && $column->getType() === Column::TYPE_USERGROUP) { $filterExpressions = []; diff --git a/tests/unit/Db/Row2MapperFilterTest.php b/tests/unit/Db/Row2MapperFilterTest.php index 9cd4d0d23c..3f5a091c07 100644 --- a/tests/unit/Db/Row2MapperFilterTest.php +++ b/tests/unit/Db/Row2MapperFilterTest.php @@ -229,6 +229,18 @@ public static function filterDataProvider(): array { 'Filter non-empty skills' ], + 'status contains-item Active,Pending' => [ + [['columnId' => 'status', 'operator' => 'contains-item', 'value' => '[0,2]']], + ['Alice', 'Charlie', 'Diana', 'Eve'], + 'Filter status containing one of Active (0) or Pending (2)' + ], + + 'skills contains-item PHP,SQL' => [ + [['columnId' => 'skills', 'operator' => 'contains-item', 'value' => '[0,2]']], + ['Alice'], + 'Filter skills containing one of PHP (0) or SQL (2)' + ], + // Selection checkbox column (is_available) 'available is checked' => [ [['columnId' => 'is_available', 'operator' => 'is-equal', 'value' => '@checked']],