1
0
Fork 0
mirror of https://gitlab.com/bramw/baserow.git synced 2025-04-07 06:15:36 +00:00

Add filters support for lookups of single select fields

This commit is contained in:
Davide Silvestri 2024-11-12 19:22:38 +00:00 committed by Bram Wiepjes
parent 5558139eb8
commit be27e89fb0
19 changed files with 1358 additions and 95 deletions
backend
src/baserow/contrib/database
tests/baserow/contrib/database/view
changelog/entries/unreleased/feature
web-frontend

View file

@ -5,9 +5,7 @@ from baserow.contrib.database.fields.dependencies.circular_reference_checker imp
)
from baserow.contrib.database.fields.field_types import FormulaFieldType
from baserow.contrib.database.fields.models import FormulaField
from baserow.contrib.database.formula.types.formula_types import (
BaserowFormulaSingleSelectType,
)
from baserow.contrib.database.fields.registries import field_type_registry
class TypeFormulaRequestSerializer(serializers.ModelSerializer):
@ -27,10 +25,14 @@ class BaserowFormulaSelectOptionsSerializer(serializers.ListField):
from baserow.contrib.database.fields.models import SelectOption
field = data.instance
if field.formula_type != BaserowFormulaSingleSelectType.type:
return []
field_type = field_type_registry.get_by_model(field)
select_options = SelectOption.objects.filter(
field_id__in=get_all_field_dependencies(field)
)
return [self.child.to_representation(item) for item in select_options]
# Select options are needed for view filters in the frontend,
# but let's avoid the potentially slow query if not required.
if field_type.can_represent_select_options(field):
select_options = SelectOption.objects.filter(
field_id__in=get_all_field_dependencies(field)
)
return [self.child.to_representation(item) for item in select_options]
else:
return []

View file

@ -443,7 +443,9 @@ class DatabaseConfig(AppConfig):
view_filter_type_registry.register(UserIsNotViewFilterType())
from .views.array_view_filters import (
HasAnySelectOptionEqualViewFilterType,
HasEmptyValueViewFilterType,
HasNoneSelectOptionEqualViewFilterType,
HasNotEmptyValueViewFilterType,
HasNotValueContainsViewFilterType,
HasNotValueContainsWordViewFilterType,
@ -463,6 +465,8 @@ class DatabaseConfig(AppConfig):
view_filter_type_registry.register(HasValueLengthIsLowerThanViewFilterType())
view_filter_type_registry.register(HasEmptyValueViewFilterType())
view_filter_type_registry.register(HasNotEmptyValueViewFilterType())
view_filter_type_registry.register(HasAnySelectOptionEqualViewFilterType())
view_filter_type_registry.register(HasNoneSelectOptionEqualViewFilterType())
from .views.view_aggregations import (
AverageViewAggregationType,

View file

@ -4890,6 +4890,9 @@ class FormulaFieldType(FormulaArrayFilterSupport, ReadOnlyFieldType):
def can_represent_files(self, field):
return self.to_baserow_formula_type(field.specific).can_represent_files
def can_represent_select_options(self, field):
return self.to_baserow_formula_type(field.specific).can_represent_select_options
def get_permission_error_when_user_changes_field_to_depend_on_forbidden_field(
self, user: AbstractUser, changed_field: Field, forbidden_field: Field
) -> Exception:
@ -5268,14 +5271,24 @@ class LookupFieldType(FormulaFieldType):
"target_field_id",
"target_field_name",
]
serializer_field_names = BASEROW_FORMULA_TYPE_ALLOWED_FIELDS + [
request_serializer_field_names = (
BASEROW_FORMULA_TYPE_REQUEST_SERIALIZER_FIELD_NAMES
+ [
"through_field_id",
"through_field_name",
"target_field_id",
"target_field_name",
"formula_type",
]
)
serializer_field_names = BASEROW_FORMULA_TYPE_SERIALIZER_FIELD_NAMES + [
"through_field_id",
"through_field_name",
"target_field_id",
"target_field_name",
"formula_type",
]
serializer_field_overrides = {
request_serializer_field_overrides = {
"through_field_name": serializers.CharField(
required=False,
allow_blank=True,
@ -5311,14 +5324,6 @@ class LookupFieldType(FormulaFieldType):
"error": serializers.CharField(required=False, read_only=True),
}
@property
def request_serializer_field_names(self):
return self.serializer_field_names
@property
def request_serializer_field_overrides(self):
return self.serializer_field_overrides
def before_create(
self, table, primary, allowed_field_values, order, user, field_kwargs
):

View file

@ -0,0 +1,96 @@
from functools import reduce
from typing import TYPE_CHECKING, List
from django.contrib.postgres.fields import JSONField
from django.db import models
from django.db.models import BooleanField, F, Q, Value
from baserow.contrib.database.fields.field_filters import (
AnnotatedQ,
OptionallyAnnotatedQ,
)
from baserow.contrib.database.formula.expression_generator.django_expressions import (
JSONArrayContainsSelectOptionValueExpr,
JSONArrayContainsSelectOptionValueSimilarToExpr,
JSONArrayEqualSelectOptionIdExpr,
)
from .base import (
HasValueContainsFilterSupport,
HasValueContainsWordFilterSupport,
HasValueEmptyFilterSupport,
HasValueFilterSupport,
)
if TYPE_CHECKING:
from baserow.contrib.database.fields.models import Field
class SingleSelectFormulaTypeFilterSupport(
HasValueEmptyFilterSupport,
HasValueFilterSupport,
HasValueContainsFilterSupport,
HasValueContainsWordFilterSupport,
):
def get_in_array_empty_query(self, field_name, model_field, field: "Field"):
return Q(**{f"{field_name}__contains": Value([{"value": None}], JSONField())})
def get_in_array_is_query(
self,
field_name: str,
value: str | List[str],
model_field: models.Field,
field: "Field",
) -> OptionallyAnnotatedQ:
if not value:
return Q()
elif isinstance(value, str):
try:
# If the value is a single value it must be a valid ID.
int(value)
except ValueError:
return Q()
value = [value]
annotations, q = {}, []
for v in value:
hashed_value = hash(v)
annotation_key = f"{field_name}_has_value_{hashed_value}"
annotation_query = JSONArrayEqualSelectOptionIdExpr(
F(field_name), Value(f"{v}"), output_field=BooleanField()
)
annotations[annotation_key] = annotation_query
q.append(Q(**{annotation_key: True}))
return AnnotatedQ(
annotation=annotations,
q=reduce(lambda a, b: a | b, q),
)
def get_in_array_contains_query(
self, field_name: str, value: str, model_field: models.Field, field: "Field"
) -> OptionallyAnnotatedQ:
annotation_query = JSONArrayContainsSelectOptionValueExpr(
F(field_name), Value(f"%{value}%"), output_field=BooleanField()
)
hashed_value = hash(value)
return AnnotatedQ(
annotation={
f"{field_name}_has_value_contains_{hashed_value}": annotation_query
},
q={f"{field_name}_has_value_contains_{hashed_value}": True},
)
def get_in_array_contains_word_query(
self, field_name: str, value: str, model_field: models.Field, field: "Field"
) -> OptionallyAnnotatedQ:
annotation_query = JSONArrayContainsSelectOptionValueSimilarToExpr(
F(field_name), Value(f"{value}"), output_field=BooleanField()
)
hashed_value = hash(value)
return AnnotatedQ(
annotation={
f"{field_name}_has_value_contains_word_{hashed_value}": annotation_query
},
q={f"{field_name}_has_value_contains_word_{hashed_value}": True},
)

View file

@ -1725,6 +1725,11 @@ class FieldType(
return False
def can_represent_select_options(self, field):
"""Indicates whether the field can be used to represent select options."""
return False
def get_permission_error_when_user_changes_field_to_depend_on_forbidden_field(
self, user: AbstractUser, changed_field: Field, forbidden_field: Field
) -> Exception:

View file

@ -209,3 +209,45 @@ class JSONArrayContainsValueLengthLowerThanExpr(BaserowFilterExpression):
""" # nosec B608 %(value)s
)
# fmt: on
class JSONArrayEqualSelectOptionIdExpr(BaserowFilterExpression):
# fmt: off
template = (
f"""
EXISTS(
SELECT filtered_field -> 'value' ->> 'id'
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
WHERE (filtered_field -> 'value' ->> 'id') LIKE (%(value)s)
)
""" # nosec B608
)
# fmt: on
class JSONArrayContainsSelectOptionValueExpr(BaserowFilterExpression):
# fmt: off
template = (
f"""
EXISTS(
SELECT filtered_field -> 'value' ->> 'value'
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
WHERE UPPER(filtered_field -> 'value' ->> 'value') LIKE UPPER(%(value)s)
)
""" # nosec B608
)
# fmt: on
class JSONArrayContainsSelectOptionValueSimilarToExpr(BaserowFilterExpression):
# fmt: off
template = (
r"""
EXISTS(
SELECT filtered_field -> 'value' ->> 'value'
FROM JSONB_ARRAY_ELEMENTS(%(field_name)s) as filtered_field
WHERE filtered_field -> 'value' ->> 'value' ~* ('\y' || %(value)s || '\y')
)
""" # nosec B608 %(value)s
)
# fmt: on

View file

@ -258,6 +258,10 @@ class BaserowFormulaType(abc.ABC):
def can_represent_files(self) -> bool:
return False
@property
def can_represent_select_options(self) -> bool:
return False
@property
def item_is_in_nested_value_object_when_in_array(self) -> bool:
return True

View file

@ -29,6 +29,9 @@ from baserow.contrib.database.fields.filter_support.base import (
from baserow.contrib.database.fields.filter_support.exceptions import (
FilterNotSupportedException,
)
from baserow.contrib.database.fields.filter_support.single_select import (
SingleSelectFormulaTypeFilterSupport,
)
from baserow.contrib.database.fields.mixins import get_date_time_format
from baserow.contrib.database.fields.utils.duration import (
D_H_M_S,
@ -1295,8 +1298,32 @@ class BaserowFormulaArrayType(
def can_represent_files(self, field):
return self.sub_type.can_represent_files(field)
def can_represent_select_options(self, field) -> bool:
return self.sub_type.can_represent_select_options(field)
class BaserowFormulaSingleSelectType(BaserowJSONBObjectBaseType):
@classmethod
def get_serializer_field_overrides(cls):
from baserow.contrib.database.api.fields.serializers import (
SelectOptionSerializer,
)
from baserow.contrib.database.api.formula.serializers import (
BaserowFormulaSelectOptionsSerializer,
)
return {
"select_options": BaserowFormulaSelectOptionsSerializer(
child=SelectOptionSerializer(),
required=False,
allow_null=True,
read_only=True,
)
}
class BaserowFormulaSingleSelectType(
SingleSelectFormulaTypeFilterSupport,
BaserowJSONBObjectBaseType,
):
type = "single_select"
baserow_field_type = "single_select"
can_order_by = True
@ -1310,6 +1337,10 @@ class BaserowFormulaSingleSelectType(BaserowJSONBObjectBaseType):
BaserowFormulaTextType,
]
@property
def can_represent_select_options(self) -> bool:
return True
@property
def limit_comparable_types(self) -> List[Type["BaserowFormulaValidType"]]:
return []

View file

@ -14,6 +14,7 @@ from baserow.contrib.database.fields.registries import field_type_registry
from baserow.contrib.database.formula import BaserowFormulaTextType
from baserow.contrib.database.formula.types.formula_types import (
BaserowFormulaCharType,
BaserowFormulaSingleSelectType,
BaserowFormulaURLType,
)
@ -33,13 +34,13 @@ class HasEmptyValueViewFilterType(ViewFilterType):
FormulaFieldType.array_of(BaserowFormulaTextType.type),
FormulaFieldType.array_of(BaserowFormulaCharType.type),
FormulaFieldType.array_of(BaserowFormulaURLType.type),
FormulaFieldType.array_of(BaserowFormulaSingleSelectType.type),
),
]
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
field_type = field_type_registry.get_by_model(field)
try:
field_type = field_type_registry.get_by_model(field)
if not isinstance(field_type, HasValueEmptyFilterSupport):
raise FilterNotSupportedException()
@ -66,13 +67,13 @@ class HasValueEqualViewFilterType(ViewFilterType):
FormulaFieldType.array_of(BaserowFormulaTextType.type),
FormulaFieldType.array_of(BaserowFormulaCharType.type),
FormulaFieldType.array_of(BaserowFormulaURLType.type),
FormulaFieldType.array_of(BaserowFormulaSingleSelectType.type),
),
]
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
field_type = field_type_registry.get_by_model(field)
try:
field_type = field_type_registry.get_by_model(field)
if not isinstance(field_type, HasValueFilterSupport):
raise FilterNotSupportedException()
@ -101,13 +102,13 @@ class HasValueContainsViewFilterType(ViewFilterType):
FormulaFieldType.array_of(BaserowFormulaTextType.type),
FormulaFieldType.array_of(BaserowFormulaCharType.type),
FormulaFieldType.array_of(BaserowFormulaURLType.type),
FormulaFieldType.array_of(BaserowFormulaSingleSelectType.type),
),
]
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
field_type = field_type_registry.get_by_model(field)
try:
field_type = field_type_registry.get_by_model(field)
if not isinstance(field_type, HasValueContainsFilterSupport):
raise FilterNotSupportedException()
@ -136,13 +137,13 @@ class HasValueContainsWordViewFilterType(ViewFilterType):
FormulaFieldType.array_of(BaserowFormulaTextType.type),
FormulaFieldType.array_of(BaserowFormulaCharType.type),
FormulaFieldType.array_of(BaserowFormulaURLType.type),
FormulaFieldType.array_of(BaserowFormulaSingleSelectType.type),
),
]
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
field_type = field_type_registry.get_by_model(field)
try:
field_type = field_type_registry.get_by_model(field)
if not isinstance(field_type, HasValueContainsWordFilterSupport):
raise FilterNotSupportedException()
@ -175,9 +176,8 @@ class HasValueLengthIsLowerThanViewFilterType(ViewFilterType):
]
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
field_type = field_type_registry.get_by_model(field)
try:
field_type = field_type_registry.get_by_model(field)
if not isinstance(field_type, HasValueLengthIsLowerThanFilterSupport):
raise FilterNotSupportedException()
@ -186,3 +186,31 @@ class HasValueLengthIsLowerThanViewFilterType(ViewFilterType):
)
except Exception:
return self.default_filter_on_exception()
class HasAnySelectOptionEqualViewFilterType(HasValueEqualViewFilterType):
"""
This filter can be used to verify if any of the select options in an array
are equal to the option IDs provided.
"""
type = "has_any_select_option_equal"
compatible_field_types = [
FormulaFieldType.compatible_with_formula_types(
FormulaFieldType.array_of(BaserowFormulaSingleSelectType.type),
),
]
def get_filter(self, field_name, value, model_field, field) -> OptionallyAnnotatedQ:
return super().get_filter(field_name, value.split(","), model_field, field)
class HasNoneSelectOptionEqualViewFilterType(
NotViewFilterTypeMixin, HasAnySelectOptionEqualViewFilterType
):
"""
This filter can be used to verify if none of the select options in an array are
equal to the option IDs provided
"""
type = "has_none_select_option_equal"

View file

@ -49,6 +49,12 @@ def uuid_field_factory(data_fixture, table, user):
return data_fixture.create_uuid_field(name="target", user=user, table=table)
def single_select_field_factory(data_fixture, table, user):
return data_fixture.create_single_select_field(
name="target", user=user, table=table
)
def setup(data_fixture, target_field_factory):
user = data_fixture.create_user()
database = data_fixture.create_database_application(user=user)
@ -85,28 +91,47 @@ def setup(data_fixture, target_field_factory):
)
def text_field_value_factory(data_fixture, target_field, value=None):
return value or ""
def single_select_field_value_factory(data_fixture, target_field, value=None):
return (
data_fixture.create_select_option(field=target_field, value=value)
if value
else None
)
@pytest.mark.parametrize(
"target_field_factory",
"target_field_factory,target_field_value_factory",
[
text_field_factory,
long_text_field_factory,
email_field_factory,
phone_number_field_factory,
url_field_factory,
(text_field_factory, text_field_value_factory),
(long_text_field_factory, text_field_value_factory),
(email_field_factory, text_field_value_factory),
(phone_number_field_factory, text_field_value_factory),
(url_field_factory, text_field_value_factory),
(single_select_field_factory, single_select_field_value_factory),
],
)
@pytest.mark.django_db
def test_has_empty_value_filter_text_field_types(data_fixture, target_field_factory):
def test_has_empty_value_filter_text_field_types(
data_fixture, target_field_factory, target_field_value_factory
):
test_setup = setup(data_fixture, target_field_factory)
row_A_value = target_field_value_factory(data_fixture, test_setup.target_field, "A")
row_B_value = target_field_value_factory(data_fixture, test_setup.target_field, "B")
row_empty_value = target_field_value_factory(data_fixture, test_setup.target_field)
other_row_A = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "A"}
**{f"field_{test_setup.target_field.id}": row_A_value}
)
other_row_B = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "B"}
**{f"field_{test_setup.target_field.id}": row_B_value}
)
other_row_empty = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": ""}
**{f"field_{test_setup.target_field.id}": row_empty_value}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
@ -189,29 +214,34 @@ def test_has_empty_value_filter_uuid_field_types(data_fixture):
@pytest.mark.parametrize(
"target_field_factory",
"target_field_factory,target_field_value_factory",
[
text_field_factory,
long_text_field_factory,
email_field_factory,
phone_number_field_factory,
url_field_factory,
(text_field_factory, text_field_value_factory),
(long_text_field_factory, text_field_value_factory),
(email_field_factory, text_field_value_factory),
(phone_number_field_factory, text_field_value_factory),
(url_field_factory, text_field_value_factory),
(single_select_field_factory, single_select_field_value_factory),
],
)
@pytest.mark.django_db
def test_has_not_empty_value_filter_text_field_types(
data_fixture, target_field_factory
data_fixture, target_field_factory, target_field_value_factory
):
test_setup = setup(data_fixture, target_field_factory)
row_A_value = target_field_value_factory(data_fixture, test_setup.target_field, "A")
row_B_value = target_field_value_factory(data_fixture, test_setup.target_field, "B")
row_empty_value = target_field_value_factory(data_fixture, test_setup.target_field)
other_row_A = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "A"}
**{f"field_{test_setup.target_field.id}": row_A_value}
)
other_row_B = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": "B"}
**{f"field_{test_setup.target_field.id}": row_B_value}
)
other_row_empty = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": ""}
**{f"field_{test_setup.target_field.id}": row_empty_value}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
@ -1560,3 +1590,540 @@ def test_has_value_length_is_lower_than_uuid_field_types(data_fixture):
).all()
]
assert len(ids) == 4
@pytest.mark.django_db
def test_has_value_equal_filter_single_select_field(data_fixture):
test_setup = setup(data_fixture, single_select_field_factory)
opt_a = data_fixture.create_select_option(field=test_setup.target_field, value="a")
opt_b = data_fixture.create_select_option(field=test_setup.target_field, value="b")
opt_c = data_fixture.create_select_option(field=test_setup.target_field, value="c")
other_row_a = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_a}
)
other_row_b = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_b}
)
other_row_c = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_c}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_a.id, other_row_b.id]
},
)
row_2 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_a.id]},
)
row_3 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_b.id, other_row_c.id]
},
)
view_filter = data_fixture.create_view_filter(
view=test_setup.grid_view,
field=test_setup.lookup_field,
type="has_value_equal",
value=str(opt_a.id),
)
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_1.id in ids
assert row_2.id in ids
view_filter.value = str(opt_b.id)
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_1.id in ids
assert row_3.id in ids
@pytest.mark.django_db
def test_has_not_value_equal_filter_single_select_field(data_fixture):
test_setup = setup(data_fixture, single_select_field_factory)
opt_a = data_fixture.create_select_option(field=test_setup.target_field, value="a")
opt_b = data_fixture.create_select_option(field=test_setup.target_field, value="b")
opt_c = data_fixture.create_select_option(field=test_setup.target_field, value="c")
other_row_a = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_a}
)
other_row_b = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_b}
)
other_row_c = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_c}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_a.id, other_row_b.id]
},
)
row_2 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_a.id]},
)
row_3 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_b.id, other_row_c.id]
},
)
view_filter = data_fixture.create_view_filter(
view=test_setup.grid_view,
field=test_setup.lookup_field,
type="has_not_value_equal",
value=str(opt_c.id),
)
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_1.id in ids
assert row_2.id in ids
view_filter.value = str(opt_b.id)
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 1
assert row_2.id in ids
@pytest.mark.django_db
def test_has_value_contains_filter_single_select_field(data_fixture):
test_setup = setup(data_fixture, single_select_field_factory)
opt_a = data_fixture.create_select_option(field=test_setup.target_field, value="a")
opt_b = data_fixture.create_select_option(field=test_setup.target_field, value="ba")
opt_c = data_fixture.create_select_option(field=test_setup.target_field, value="c")
other_row_a = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_a}
)
other_row_b = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_b}
)
other_row_c = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_c}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_a.id, other_row_b.id]
},
)
row_2 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_a.id]},
)
row_3 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_b.id, other_row_c.id]
},
)
view_filter = data_fixture.create_view_filter(
view=test_setup.grid_view,
field=test_setup.lookup_field,
type="has_value_contains",
value="a",
)
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 3
assert row_1.id in ids
assert row_2.id in ids
assert row_3.id in ids
view_filter.value = "c"
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 1
assert row_3.id in ids
@pytest.mark.django_db
def test_has_not_value_contains_filter_single_select_field(data_fixture):
test_setup = setup(data_fixture, single_select_field_factory)
opt_a = data_fixture.create_select_option(field=test_setup.target_field, value="a")
opt_b = data_fixture.create_select_option(field=test_setup.target_field, value="ba")
opt_c = data_fixture.create_select_option(field=test_setup.target_field, value="c")
other_row_a = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_a}
)
other_row_b = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_b}
)
other_row_c = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_c}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_a.id, other_row_b.id]
},
)
row_2 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_a.id]},
)
row_3 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_b.id, other_row_c.id]
},
)
view_filter = data_fixture.create_view_filter(
view=test_setup.grid_view,
field=test_setup.lookup_field,
type="has_not_value_contains",
value="a",
)
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 0
view_filter.value = "c"
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_1.id in ids
assert row_2.id in ids
@pytest.mark.django_db
def test_has_value_contains_word_filter_single_select_field(data_fixture):
test_setup = setup(data_fixture, single_select_field_factory)
opt_a = data_fixture.create_select_option(field=test_setup.target_field, value="a")
opt_b = data_fixture.create_select_option(field=test_setup.target_field, value="b")
opt_c = data_fixture.create_select_option(field=test_setup.target_field, value="ca")
other_row_a = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_a}
)
other_row_b = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_b}
)
other_row_c = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_c}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_a.id, other_row_b.id]
},
)
row_2 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_a.id]},
)
row_3 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_b.id, other_row_c.id]
},
)
view_filter = data_fixture.create_view_filter(
view=test_setup.grid_view,
field=test_setup.lookup_field,
type="has_value_contains_word",
value="a",
)
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_1.id in ids
assert row_2.id in ids
view_filter.value = "ca"
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 1
assert row_3.id in ids
@pytest.mark.django_db
def test_has_not_value_contains_word_filter_single_select_field(data_fixture):
test_setup = setup(data_fixture, single_select_field_factory)
opt_a = data_fixture.create_select_option(field=test_setup.target_field, value="a")
opt_b = data_fixture.create_select_option(field=test_setup.target_field, value="b")
opt_c = data_fixture.create_select_option(field=test_setup.target_field, value="ca")
other_row_a = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_a}
)
other_row_b = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_b}
)
other_row_c = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_c}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_a.id, other_row_b.id]
},
)
row_2 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_a.id]},
)
row_3 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_b.id, other_row_c.id]
},
)
view_filter = data_fixture.create_view_filter(
view=test_setup.grid_view,
field=test_setup.lookup_field,
type="has_not_value_contains_word",
value="a",
)
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 1
assert row_3.id in ids
view_filter.value = "ca"
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_1.id in ids
assert row_2.id in ids
@pytest.mark.django_db
def test_has_any_select_option_equal_filter_single_select_field(data_fixture):
test_setup = setup(data_fixture, single_select_field_factory)
opt_a = data_fixture.create_select_option(field=test_setup.target_field, value="a")
opt_b = data_fixture.create_select_option(field=test_setup.target_field, value="b")
opt_c = data_fixture.create_select_option(field=test_setup.target_field, value="c")
other_row_a = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_a}
)
other_row_b = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_b}
)
other_row_c = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_c}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_a.id, other_row_b.id]
},
)
row_2 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_a.id]},
)
row_3 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_b.id, other_row_c.id]
},
)
view_filter = data_fixture.create_view_filter(
view=test_setup.grid_view,
field=test_setup.lookup_field,
type="has_any_select_option_equal",
value=f"{opt_a.id},{opt_c.id}",
)
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 3
assert row_1.id in ids
assert row_2.id in ids
assert row_3.id in ids
view_filter.value = f"{opt_b.id},{opt_c.id}"
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 2
assert row_1.id in ids
assert row_3.id in ids
@pytest.mark.django_db
def test_has_none_select_option_equal_filter_single_select_field(data_fixture):
test_setup = setup(data_fixture, single_select_field_factory)
opt_a = data_fixture.create_select_option(field=test_setup.target_field, value="a")
opt_b = data_fixture.create_select_option(field=test_setup.target_field, value="b")
opt_c = data_fixture.create_select_option(field=test_setup.target_field, value="ca")
other_row_a = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_a}
)
other_row_b = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_b}
)
other_row_c = test_setup.other_table_model.objects.create(
**{f"field_{test_setup.target_field.id}": opt_c}
)
row_1 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_a.id, other_row_b.id]
},
)
row_2 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={f"field_{test_setup.link_row_field.id}": [other_row_a.id]},
)
row_3 = test_setup.row_handler.create_row(
user=test_setup.user,
table=test_setup.table,
values={
f"field_{test_setup.link_row_field.id}": [other_row_b.id, other_row_c.id]
},
)
view_filter = data_fixture.create_view_filter(
view=test_setup.grid_view,
field=test_setup.lookup_field,
type="has_none_select_option_equal",
value=f"{opt_a.id},{opt_c.id}",
)
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 0
view_filter.value = f"{opt_b.id},{opt_c.id}"
view_filter.save()
ids = [
r.id
for r in test_setup.view_handler.apply_filters(
test_setup.grid_view, test_setup.model.objects.all()
).all()
]
assert len(ids) == 1
assert row_2.id in ids

View file

@ -0,0 +1,7 @@
{
"type": "feature",
"message": "Add filters support for lookups of single select fields.",
"issue_number": 3182,
"bullet_points": [],
"created_at": "2024-11-06"
}

View file

@ -198,6 +198,8 @@
"hasValueContainsWord": "has value contains word",
"hasNotValueContainsWord": "doesn't have value contains word",
"hasValueLengthIsLowerThan": "has value length is lower than",
"hasAnySelectOptionEqual": "has any select option equal",
"hasNoneSelectOptionEqual": "doesn't have select option equal",
"contains": "contains",
"containsNot": "doesn't contain",
"containsWord": "contains word",

View file

@ -36,14 +36,16 @@
export function mix(...chain) {
const [baseClass, ...mixins] = chain.reverse()
class Mixed extends baseClass {}
for (const mixin of mixins) {
for (const [key, value] of Object.entries(mixin)) {
/* eslint no-prototype-builtins: "off" */
if (!baseClass.prototype.hasOwnProperty(key)) {
baseClass.prototype[key] = value
if (!Mixed.prototype.hasOwnProperty(key)) {
Mixed.prototype[key] = value
}
}
}
return baseClass
return Mixed
}

View file

@ -71,3 +71,105 @@ export const hasValueLengthIsLowerThanFilterMixin = {
return genericHasValueLengthLowerThanFilter
},
}
export const formulaArrayFilterMixin = {
getSubType(field) {
return this.app.$registry.get('formula_type', field.array_formula_type)
},
getHasEmptyValueFilterFunction(field) {
const subType = this.getSubType(field)
return subType.getHasEmptyValueFilterFunction(field)
},
getHasValueLengthIsLowerThanFilterFunction(field) {
const subType = this.getSubType(field)
return subType.getHasValueLengthIsLowerThanFilterFunction(field)
},
getHasValueContainsFilterFunction(field) {
const subType = this.getSubType(field)
return subType.getHasValueContainsFilterFunction(field)
},
getHasValueContainsWordFilterFunction(field) {
const subType = this.getSubType(field)
return subType.getHasValueContainsWordFilterFunction(field)
},
hasValueContainsWordFilter(cellValue, filterValue, field) {
const subType = this.getSubType(field)
return subType.hasValueContainsWordFilter(cellValue, filterValue, field)
},
hasNotValueContainsWordFilter(cellValue, filterValue, field) {
const subType = this.getSubType(field)
return subType.hasNotValueContainsWordFilter(cellValue, filterValue, field)
},
getHasValueEqualFilterFunction(field) {
const subType = this.getSubType(field)
return subType.getHasValueEqualFilterFunction(field)
},
hasValueEqualFilter(cellValue, filterValue, field) {
const subType = this.getSubType(field)
return subType.hasValueEqualFilter(cellValue, filterValue, field)
},
hasNotValueEqualFilter(cellValue, filterValue, field) {
const subType = this.getSubType(field)
return subType.hasNotValueEqualFilter(cellValue, filterValue, field)
},
}
export const hasSelectOptionIdEqualMixin = Object.assign(
{},
hasValueEqualFilterMixin,
{
getHasValueEqualFilterFunction(field) {
const mapOptionIdsToValues = (cellVal) =>
cellVal.map((v) => ({
id: v.id,
value: String(v.value?.id || ''),
}))
const hasValueEqualFilter = (cellVal, fltValue) =>
genericHasValueEqualFilter(mapOptionIdsToValues(cellVal), fltValue)
return (cellValue, filterValue) => {
const filterValues = filterValue.trim().split(',')
return filterValues.reduce((acc, fltValue) => {
return acc || hasValueEqualFilter(cellValue, String(fltValue))
}, false)
}
},
}
)
export const hasSelectOptionValueContainsFilterMixin = Object.assign(
{},
hasValueContainsFilterMixin,
{
getHasValueContainsFilterFunction(field) {
return (cellValue, filterValue) =>
genericHasValueContainsFilter(
cellValue.map((v) => ({ id: v.id, value: v.value?.value || '' })),
filterValue
)
},
}
)
export const hasSelectOptionValueContainsWordFilterMixin = Object.assign(
{},
hasValueContainsWordFilterMixin,
{
getHasValueContainsWordFilterFunction(field) {
return (cellValue, filterValue) =>
genericHasValueContainsWordFilter(
cellValue.map((v) => ({ id: v.id, value: v.value?.value || '' })),
filterValue
)
},
}
)

View file

@ -2,6 +2,8 @@ import ViewFilterTypeText from '@baserow/modules/database/components/view/ViewFi
import ViewFilterTypeNumber from '@baserow/modules/database/components/view/ViewFilterTypeNumber'
import { FormulaFieldType } from '@baserow/modules/database/fieldTypes'
import { ViewFilterType } from '@baserow/modules/database/viewFilters'
import ViewFilterTypeSelectOptions from '@baserow/modules/database/components/view/ViewFilterTypeSelectOptions'
import ViewFilterTypeMultipleSelectOptions from '@baserow/modules/database/components/view/ViewFilterTypeMultipleSelectOptions'
export class HasEmptyValueViewFilterType extends ViewFilterType {
static getType() {
@ -18,6 +20,7 @@ export class HasEmptyValueViewFilterType extends ViewFilterType {
FormulaFieldType.compatibleWithFormulaTypes('array(text)'),
FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
]
}
@ -41,6 +44,7 @@ export class HasNotEmptyValueViewFilterType extends ViewFilterType {
FormulaFieldType.compatibleWithFormulaTypes('array(text)'),
FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
]
}
@ -60,7 +64,10 @@ export class HasValueEqualViewFilterType extends ViewFilterType {
}
getInputComponent(field) {
return ViewFilterTypeText
const mapping = {
single_select: ViewFilterTypeSelectOptions,
}
return mapping[field.array_formula_type] || ViewFilterTypeText
}
getCompatibleFieldTypes() {
@ -68,6 +75,7 @@ export class HasValueEqualViewFilterType extends ViewFilterType {
FormulaFieldType.compatibleWithFormulaTypes('array(text)'),
FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
]
}
@ -87,7 +95,10 @@ export class HasNotValueEqualViewFilterType extends ViewFilterType {
}
getInputComponent(field) {
return ViewFilterTypeText
const mapping = {
single_select: ViewFilterTypeSelectOptions,
}
return mapping[field.array_formula_type] || ViewFilterTypeText
}
getCompatibleFieldTypes() {
@ -95,6 +106,7 @@ export class HasNotValueEqualViewFilterType extends ViewFilterType {
FormulaFieldType.compatibleWithFormulaTypes('array(text)'),
FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
]
}
@ -122,6 +134,7 @@ export class HasValueContainsViewFilterType extends ViewFilterType {
FormulaFieldType.compatibleWithFormulaTypes('array(text)'),
FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
]
}
@ -149,6 +162,7 @@ export class HasNotValueContainsViewFilterType extends ViewFilterType {
FormulaFieldType.compatibleWithFormulaTypes('array(text)'),
FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
]
}
@ -176,6 +190,7 @@ export class HasValueContainsWordViewFilterType extends ViewFilterType {
FormulaFieldType.compatibleWithFormulaTypes('array(text)'),
FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
]
}
@ -203,6 +218,7 @@ export class HasNotValueContainsWordViewFilterType extends ViewFilterType {
FormulaFieldType.compatibleWithFormulaTypes('array(text)'),
FormulaFieldType.compatibleWithFormulaTypes('array(char)'),
FormulaFieldType.compatibleWithFormulaTypes('array(url)'),
FormulaFieldType.compatibleWithFormulaTypes('array(single_select)'),
]
}
@ -244,3 +260,49 @@ export class HasValueLengthIsLowerThanViewFilterType extends ViewFilterType {
)
}
}
export class HasAnySelectOptionEqualViewFilterType extends ViewFilterType {
static getType() {
return 'has_any_select_option_equal'
}
getName() {
const { i18n } = this.app
return i18n.t('viewFilter.hasAnySelectOptionEqual')
}
getInputComponent(field) {
return ViewFilterTypeMultipleSelectOptions
}
getCompatibleFieldTypes() {
return [FormulaFieldType.compatibleWithFormulaTypes('array(single_select)')]
}
matches(cellValue, filterValue, field, fieldType) {
return fieldType.hasValueEqualFilter(cellValue, filterValue, field)
}
}
export class HasNoneSelectOptionEqualViewFilterType extends ViewFilterType {
static getType() {
return 'has_none_select_option_equal'
}
getName() {
const { i18n } = this.app
return i18n.t('viewFilter.hasNoneSelectOptionEqual')
}
getInputComponent(field) {
return ViewFilterTypeMultipleSelectOptions
}
getCompatibleFieldTypes() {
return [FormulaFieldType.compatibleWithFormulaTypes('array(single_select)')]
}
matches(cellValue, filterValue, field, fieldType) {
return fieldType.hasNotValueEqualFilter(cellValue, filterValue, field)
}
}

View file

@ -53,6 +53,10 @@ import {
hasValueContainsFilterMixin,
hasValueContainsWordFilterMixin,
hasValueLengthIsLowerThanFilterMixin,
hasSelectOptionIdEqualMixin,
hasSelectOptionValueContainsFilterMixin,
hasSelectOptionValueContainsWordFilterMixin,
formulaArrayFilterMixin,
} from '@baserow/modules/database/arrayFilterMixins'
import _ from 'lodash'
@ -517,11 +521,7 @@ export class BaserowFormulaInvalidType extends BaserowFormulaTypeDefinition {
}
export class BaserowFormulaArrayType extends mix(
hasEmptyValueFilterMixin,
hasValueEqualFilterMixin,
hasValueContainsFilterMixin,
hasValueContainsWordFilterMixin,
hasValueLengthIsLowerThanFilterMixin,
formulaArrayFilterMixin,
BaserowFormulaTypeDefinition
) {
static getType() {
@ -705,38 +705,6 @@ export class BaserowFormulaArrayType extends mix(
)
return subType.getHasEmptyValueFilterFunction(field)
}
getHasValueEqualFilterFunction(field) {
const subType = this.app.$registry.get(
'formula_type',
field.array_formula_type
)
return subType.getHasValueEqualFilterFunction(field)
}
getHasValueContainsFilterFunction(field) {
const subType = this.app.$registry.get(
'formula_type',
field.array_formula_type
)
return subType.getHasValueContainsFilterFunction(field)
}
getHasValueContainsWordFilterFunction(field) {
const subType = this.app.$registry.get(
'formula_type',
field.array_formula_type
)
return subType.getHasValueContainsWordFilterFunction(field)
}
getHasValueLengthIsLowerThanFilterFunction(field) {
const subType = this.app.$registry.get(
'formula_type',
field.array_formula_type
)
return subType.getHasValueLengthIsLowerThanFilterFunction(field)
}
}
export class BaserowFormulaFileType extends BaserowFormulaTypeDefinition {
@ -842,7 +810,13 @@ export class BaserowFormulaFileType extends BaserowFormulaTypeDefinition {
}
}
export class BaserowFormulaSingleSelectType extends BaserowFormulaTypeDefinition {
export class BaserowFormulaSingleSelectType extends mix(
hasEmptyValueFilterMixin,
hasSelectOptionIdEqualMixin,
hasSelectOptionValueContainsFilterMixin,
hasSelectOptionValueContainsWordFilterMixin,
BaserowFormulaTypeDefinition
) {
static getType() {
return 'single_select'
}
@ -984,7 +958,14 @@ export class BaserowFormulaLinkType extends BaserowFormulaTypeDefinition {
}
}
export class BaserowFormulaURLType extends BaserowFormulaTypeDefinition {
export class BaserowFormulaURLType extends mix(
hasEmptyValueFilterMixin,
hasValueEqualFilterMixin,
hasValueContainsFilterMixin,
hasValueContainsWordFilterMixin,
hasValueLengthIsLowerThanFilterMixin,
BaserowFormulaTypeDefinition
) {
static getType() {
return 'url'
}

View file

@ -105,6 +105,8 @@ import {
HasValueContainsWordViewFilterType,
HasNotValueContainsWordViewFilterType,
HasValueLengthIsLowerThanViewFilterType,
HasAnySelectOptionEqualViewFilterType,
HasNoneSelectOptionEqualViewFilterType,
} from '@baserow/modules/database/arrayViewFilters'
import {
CSVImporterType,
@ -487,6 +489,14 @@ export default (context) => {
'viewFilter',
new HasValueLengthIsLowerThanViewFilterType(context)
)
app.$registry.register(
'viewFilter',
new HasAnySelectOptionEqualViewFilterType(context)
)
app.$registry.register(
'viewFilter',
new HasNoneSelectOptionEqualViewFilterType(context)
)
app.$registry.register('viewFilter', new ContainsViewFilterType(context))
app.$registry.register('viewFilter', new ContainsNotViewFilterType(context))
app.$registry.register('viewFilter', new ContainsWordViewFilterType(context))

View file

@ -59,7 +59,7 @@ export function genericHasEmptyValueFilter(cellValue, filterValue) {
for (let i = 0; i < cellValue.length; i++) {
const value = cellValue[i].value
if (value === '') {
if (value === '' || value === null) {
return true
}
}

View file

@ -435,4 +435,317 @@ describe('Text-based array view filters', () => {
)
}
)
const hasSelectOptionsEqualCases = [
{
cellValue: [],
filterValue: '1',
expected: false,
},
{
cellValue: [
{ value: { id: 2, value: 'B' } },
{ value: { id: 1, value: 'A' } },
],
filterValue: '1',
expected: true,
},
{
cellValue: [{ value: { id: 1, value: 'A' } }],
filterValue: '2',
expected: false,
},
{
cellValue: [{ value: { id: 3, value: 'Aa' } }],
filterValue: '1',
expected: false,
},
]
const hasSelectOptionEqualSupportedFields = [
{
TestFieldType: FormulaFieldType,
formula_type: 'array',
array_formula_type: 'single_select',
},
]
describe.each(hasSelectOptionEqualSupportedFields)(
'HasValueEqualViewFilterType %j',
(field) => {
test.each(hasSelectOptionsEqualCases)(
'filter matches values %j',
(testValues) => {
const fieldType = new field.TestFieldType({
app: testApp._app,
})
const result = new HasValueEqualViewFilterType({
app: testApp._app,
}).matches(
testValues.cellValue,
testValues.filterValue,
field,
fieldType
)
expect(result).toBe(testValues.expected)
}
)
}
)
describe.each(hasSelectOptionEqualSupportedFields)(
'HasNotValueEqualViewFilterType %j',
(field) => {
test.each(hasSelectOptionsEqualCases)(
'filter not matches values %j',
(testValues) => {
const fieldType = new field.TestFieldType({
app: testApp._app,
})
const result = new HasNotValueEqualViewFilterType({
app: testApp._app,
}).matches(
testValues.cellValue,
testValues.filterValue,
field,
fieldType
)
expect(result).toBe(!testValues.expected)
}
)
}
)
const hasSelectOptionContainsCases = [
{
cellValue: [],
filterValue: 'A',
expected: false,
},
{
cellValue: [
{ value: { id: 2, value: 'B' } },
{ value: { id: 1, value: 'A' } },
],
filterValue: 'A',
expected: true,
},
{
cellValue: [{ value: { id: 1, value: 'A' } }],
filterValue: 'B',
expected: false,
},
{
cellValue: [{ value: { id: 3, value: 'Aa' } }],
filterValue: 'a',
expected: true,
},
]
const hasSelectOptionContainsSupportedFields = [
{
TestFieldType: FormulaFieldType,
formula_type: 'array',
array_formula_type: 'single_select',
},
]
describe.each(hasSelectOptionContainsSupportedFields)(
'HasValueContainsViewFilterType %j',
(field) => {
test.each(hasSelectOptionContainsCases)(
'filter matches values %j',
(testValues) => {
const fieldType = new field.TestFieldType({
app: testApp._app,
})
const result = new HasValueContainsViewFilterType({
app: testApp._app,
}).matches(
testValues.cellValue,
testValues.filterValue,
field,
fieldType
)
expect(result).toBe(testValues.expected)
}
)
}
)
describe.each(hasSelectOptionContainsSupportedFields)(
'HasNotValueContainsViewFilterType %j',
(field) => {
test.each(hasSelectOptionContainsCases)(
'filter not matches values %j',
(testValues) => {
const fieldType = new field.TestFieldType({
app: testApp._app,
})
const result = new HasNotValueContainsViewFilterType({
app: testApp._app,
}).matches(
testValues.cellValue,
testValues.filterValue,
field,
fieldType
)
expect(result).toBe(!testValues.expected)
}
)
}
)
const hasSelectOptionContainsWordCases = [
{
cellValue: [],
filterValue: 'A',
expected: false,
},
{
cellValue: [
{ value: { id: 2, value: 'B' } },
{ value: { id: 1, value: 'Aa' } },
],
filterValue: 'Aa',
expected: true,
},
{
cellValue: [{ value: { id: 1, value: 'A' } }],
filterValue: 'B',
expected: false,
},
{
cellValue: [{ value: { id: 3, value: 'Aa' } }],
filterValue: 'a',
expected: false,
},
]
const hasSelectOptionsContainsWordSupportedFields = [
{
TestFieldType: FormulaFieldType,
formula_type: 'array',
array_formula_type: 'single_select',
},
]
describe.each(hasSelectOptionsContainsWordSupportedFields)(
'HasValueContainsWordViewFilterType %j',
(field) => {
test.each(hasSelectOptionContainsWordCases)(
'filter matches values %j',
(testValues) => {
const fieldType = new field.TestFieldType({
app: testApp._app,
})
const result = new HasValueContainsWordViewFilterType({
app: testApp._app,
}).matches(
testValues.cellValue,
testValues.filterValue,
field,
fieldType
)
expect(result).toBe(testValues.expected)
}
)
}
)
describe.each(hasSelectOptionsContainsWordSupportedFields)(
'HasNotValueContainsWordViewFilterType %j',
(field) => {
test.each(hasSelectOptionContainsWordCases)(
'filter not matches values %j',
(testValues) => {
const fieldType = new field.TestFieldType({
app: testApp._app,
})
const result = new HasNotValueContainsWordViewFilterType({
app: testApp._app,
}).matches(
testValues.cellValue,
testValues.filterValue,
field,
fieldType
)
expect(result).toBe(!testValues.expected)
}
)
}
)
const hasEmptySelectOptionsCases = [
{
cellValue: [],
expected: false,
},
{
cellValue: [{ value: { id: 1, value: 'a' } }, { value: null }],
expected: true,
},
{
cellValue: [{ value: null }],
expected: true,
},
{
cellValue: [{ value: { id: 2, value: 'b' } }],
expected: false,
},
]
const hasEmptySelectOptionSupportedFields = [
{
TestFieldType: FormulaFieldType,
formula_type: 'array',
array_formula_type: 'single_select',
},
]
describe.each(hasEmptySelectOptionSupportedFields)(
'HasEmptyValueViewFilterType %j',
(field) => {
test.each(hasEmptySelectOptionsCases)(
'filter not matches values %j',
(testValues) => {
const fieldType = new field.TestFieldType({
app: testApp._app,
})
const result = new HasEmptyValueViewFilterType({
app: testApp._app,
}).matches(
testValues.cellValue,
testValues.filterValue,
field,
fieldType
)
expect(result).toBe(testValues.expected)
}
)
}
)
describe.each(hasEmptySelectOptionSupportedFields)(
'HasNotEmptyValueViewFilterType %j',
(field) => {
test.each(hasEmptySelectOptionsCases)(
'filter not matches values %j',
(testValues) => {
const fieldType = new field.TestFieldType({
app: testApp._app,
})
const result = new HasNotEmptyValueViewFilterType({
app: testApp._app,
}).matches(
testValues.cellValue,
testValues.filterValue,
field,
fieldType
)
expect(result).toBe(!testValues.expected)
}
)
}
)
})