mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-14 09:08:32 +00:00
🌈 3️⃣ - Row coloring v3 - Add condition value provider
This commit is contained in:
parent
bc1685e1c3
commit
8e273e6960
38 changed files with 2518 additions and 1337 deletions
backend
src/baserow/contrib/database
tests/baserow/contrib/database
premium
backend
src/baserow_premium
tests/baserow_premium/views
web-frontend/modules/baserow_premium
web-frontend
20
backend/src/baserow/contrib/database/api/rows/schemas.py
Normal file
20
backend/src/baserow/contrib/database/api/rows/schemas.py
Normal file
|
@ -0,0 +1,20 @@
|
||||||
|
from drf_spectacular.plumbing import build_object_type
|
||||||
|
|
||||||
|
|
||||||
|
row_names_response_schema = build_object_type(
|
||||||
|
{
|
||||||
|
"{table_id}*": {
|
||||||
|
"type": "object",
|
||||||
|
"description": "An object containing the row names of table `table_id`.",
|
||||||
|
"properties": {
|
||||||
|
"{row_id}*": {
|
||||||
|
"type": "string",
|
||||||
|
"description": (
|
||||||
|
"the name of the row with id `row_id` from table "
|
||||||
|
"with id `table_id`."
|
||||||
|
),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
)
|
|
@ -1,6 +1,6 @@
|
||||||
from django.urls import re_path
|
from django.urls import re_path
|
||||||
|
|
||||||
from .views import RowsView, RowView, RowMoveView, BatchRowsView
|
from .views import RowsView, RowView, RowMoveView, RowNamesView, BatchRowsView
|
||||||
|
|
||||||
|
|
||||||
app_name = "baserow.contrib.database.api.rows"
|
app_name = "baserow.contrib.database.api.rows"
|
||||||
|
@ -22,4 +22,9 @@ urlpatterns = [
|
||||||
RowMoveView.as_view(),
|
RowMoveView.as_view(),
|
||||||
name="move",
|
name="move",
|
||||||
),
|
),
|
||||||
|
re_path(
|
||||||
|
r"names/$",
|
||||||
|
RowNamesView.as_view(),
|
||||||
|
name="names",
|
||||||
|
),
|
||||||
]
|
]
|
||||||
|
|
|
@ -10,7 +10,10 @@ from rest_framework.views import APIView
|
||||||
|
|
||||||
from baserow.api.decorators import map_exceptions, validate_query_parameters
|
from baserow.api.decorators import map_exceptions, validate_query_parameters
|
||||||
from baserow.api.errors import ERROR_USER_NOT_IN_GROUP
|
from baserow.api.errors import ERROR_USER_NOT_IN_GROUP
|
||||||
from baserow.api.exceptions import RequestBodyValidationException
|
from baserow.api.exceptions import (
|
||||||
|
RequestBodyValidationException,
|
||||||
|
QueryParameterValidationException,
|
||||||
|
)
|
||||||
from baserow.api.pagination import PageNumberPagination
|
from baserow.api.pagination import PageNumberPagination
|
||||||
from baserow.api.schemas import get_error_schema, CLIENT_SESSION_ID_SCHEMA_PARAMETER
|
from baserow.api.schemas import get_error_schema, CLIENT_SESSION_ID_SCHEMA_PARAMETER
|
||||||
from baserow.api.trash.errors import ERROR_CANNOT_DELETE_ALREADY_DELETED_ITEM
|
from baserow.api.trash.errors import ERROR_CANNOT_DELETE_ALREADY_DELETED_ITEM
|
||||||
|
@ -56,6 +59,7 @@ from baserow.contrib.database.rows.exceptions import RowDoesNotExist, RowIdsNotU
|
||||||
from baserow.contrib.database.rows.handler import RowHandler
|
from baserow.contrib.database.rows.handler import RowHandler
|
||||||
from baserow.contrib.database.table.exceptions import TableDoesNotExist
|
from baserow.contrib.database.table.exceptions import TableDoesNotExist
|
||||||
from baserow.contrib.database.table.handler import TableHandler
|
from baserow.contrib.database.table.handler import TableHandler
|
||||||
|
from baserow.contrib.database.table.models import Table
|
||||||
from baserow.contrib.database.tokens.exceptions import NoPermissionToTable
|
from baserow.contrib.database.tokens.exceptions import NoPermissionToTable
|
||||||
from baserow.contrib.database.tokens.handler import TokenHandler
|
from baserow.contrib.database.tokens.handler import TokenHandler
|
||||||
from baserow.contrib.database.views.exceptions import (
|
from baserow.contrib.database.views.exceptions import (
|
||||||
|
@ -80,6 +84,7 @@ from baserow.contrib.database.fields.field_filters import (
|
||||||
FILTER_TYPE_AND,
|
FILTER_TYPE_AND,
|
||||||
FILTER_TYPE_OR,
|
FILTER_TYPE_OR,
|
||||||
)
|
)
|
||||||
|
from .schemas import row_names_response_schema
|
||||||
|
|
||||||
|
|
||||||
class RowsView(APIView):
|
class RowsView(APIView):
|
||||||
|
@ -428,6 +433,109 @@ class RowsView(APIView):
|
||||||
return Response(serializer.data)
|
return Response(serializer.data)
|
||||||
|
|
||||||
|
|
||||||
|
class RowNamesView(APIView):
|
||||||
|
authentication_classes = APIView.authentication_classes + [TokenAuthentication]
|
||||||
|
permission_classes = (IsAuthenticated,)
|
||||||
|
|
||||||
|
@extend_schema(
|
||||||
|
parameters=[
|
||||||
|
OpenApiParameter(
|
||||||
|
name="table__{id}",
|
||||||
|
location=OpenApiParameter.QUERY,
|
||||||
|
type=OpenApiTypes.STR,
|
||||||
|
description=(
|
||||||
|
"A list of comma separated row ids to query from the table with "
|
||||||
|
"id {id}. For example, if you "
|
||||||
|
"want the name of row `42` and `43` from table `28` this parameter "
|
||||||
|
"will be `table__28=42,43`. You can specify multiple rows for "
|
||||||
|
"different tables but every tables must be in the same database. "
|
||||||
|
"You need at least read permission on all specified tables."
|
||||||
|
),
|
||||||
|
),
|
||||||
|
],
|
||||||
|
tags=["Database table rows"],
|
||||||
|
operation_id="list_database_table_row_names",
|
||||||
|
description=(
|
||||||
|
"Returns the names of the given row of the given tables. The name"
|
||||||
|
"of a row is the primary field value for this row. The result can be used"
|
||||||
|
"for example, when you want to display the name of a linked row from "
|
||||||
|
"another table."
|
||||||
|
),
|
||||||
|
responses={
|
||||||
|
200: row_names_response_schema,
|
||||||
|
400: get_error_schema(
|
||||||
|
[
|
||||||
|
"ERROR_USER_NOT_IN_GROUP",
|
||||||
|
]
|
||||||
|
),
|
||||||
|
401: get_error_schema(["ERROR_NO_PERMISSION_TO_TABLE"]),
|
||||||
|
404: get_error_schema(["ERROR_TABLE_DOES_NOT_EXIST"]),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
@map_exceptions(
|
||||||
|
{
|
||||||
|
UserNotInGroup: ERROR_USER_NOT_IN_GROUP,
|
||||||
|
TableDoesNotExist: ERROR_TABLE_DOES_NOT_EXIST,
|
||||||
|
NoPermissionToTable: ERROR_NO_PERMISSION_TO_TABLE,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
def get(self, request):
|
||||||
|
"""
|
||||||
|
Returns the names (i.e. primary field value) of specified rows of given tables.
|
||||||
|
Can be used when you want to display a row name referenced from another table.
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = {}
|
||||||
|
database = None
|
||||||
|
table_handler = TableHandler()
|
||||||
|
token_handler = TokenHandler()
|
||||||
|
row_handler = RowHandler()
|
||||||
|
|
||||||
|
for name, value in request.GET.items():
|
||||||
|
if not name.startswith("table__"):
|
||||||
|
raise QueryParameterValidationException(
|
||||||
|
detail='Only table Id prefixed by "table__" are allowed as parameter.',
|
||||||
|
code="invalid_parameter",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
table_id = int(name[7:])
|
||||||
|
except ValueError:
|
||||||
|
raise QueryParameterValidationException(
|
||||||
|
detail=(f'Failed to parse table id in "{name}".'),
|
||||||
|
code="invalid_table_id",
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
row_ids = [int(id) for id in value.split(",")]
|
||||||
|
except ValueError:
|
||||||
|
raise QueryParameterValidationException(
|
||||||
|
detail=(
|
||||||
|
f'Failed to parse row ids in "{value}" for '
|
||||||
|
f'"table__{table_id}" parameter.'
|
||||||
|
),
|
||||||
|
code="invalid_row_ids",
|
||||||
|
)
|
||||||
|
|
||||||
|
table_queryset = None
|
||||||
|
if database:
|
||||||
|
# Once we have the database, we want only tables from the same database
|
||||||
|
table_queryset = Table.objects.filter(database=database)
|
||||||
|
|
||||||
|
table = table_handler.get_table(table_id, base_queryset=table_queryset)
|
||||||
|
|
||||||
|
if not database:
|
||||||
|
# Check permission once
|
||||||
|
database = table.database
|
||||||
|
database.group.has_user(request.user, raise_error=True)
|
||||||
|
|
||||||
|
token_handler.check_table_permissions(request, "read", table, False)
|
||||||
|
|
||||||
|
result[table_id] = row_handler.get_row_names(table, row_ids)
|
||||||
|
|
||||||
|
return Response(result)
|
||||||
|
|
||||||
|
|
||||||
class RowView(APIView):
|
class RowView(APIView):
|
||||||
authentication_classes = APIView.authentication_classes + [TokenAuthentication]
|
authentication_classes = APIView.authentication_classes + [TokenAuthentication]
|
||||||
permission_classes = (IsAuthenticated,)
|
permission_classes = (IsAuthenticated,)
|
||||||
|
|
|
@ -306,6 +306,31 @@ class RowHandler:
|
||||||
|
|
||||||
return cast(GeneratedTableModelForUpdate, row)
|
return cast(GeneratedTableModelForUpdate, row)
|
||||||
|
|
||||||
|
def get_row_names(
|
||||||
|
self, table: "Table", row_ids: List[int], model: "GeneratedTableModel" = None
|
||||||
|
) -> Dict[str, int]:
|
||||||
|
"""
|
||||||
|
Returns the row names for all row ids specified in `row_ids` parameter from
|
||||||
|
the given table.
|
||||||
|
|
||||||
|
:param table: The table where the rows must be fetched from.
|
||||||
|
:param row_ids: The id of the rows that must be fetched.
|
||||||
|
:param model: If the correct model has already been generated it can be
|
||||||
|
provided so that it does not have to be generated for a second time.
|
||||||
|
:return: A dict of the requested rows names. The key are the row ids and the
|
||||||
|
values are the row names.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if not model:
|
||||||
|
primary_field = table.field_set.get(primary=True)
|
||||||
|
model = table.get_model(
|
||||||
|
field_ids=[], fields=[primary_field], add_dependencies=False
|
||||||
|
)
|
||||||
|
|
||||||
|
queryset = model.objects.filter(pk__in=row_ids)
|
||||||
|
|
||||||
|
return {row.id: str(row) for row in queryset}
|
||||||
|
|
||||||
# noinspection PyMethodMayBeStatic
|
# noinspection PyMethodMayBeStatic
|
||||||
def has_row(self, user, table, row_id, raise_error=False, model=None):
|
def has_row(self, user, table, row_id, raise_error=False, model=None):
|
||||||
"""
|
"""
|
||||||
|
@ -313,7 +338,7 @@ class RowHandler:
|
||||||
|
|
||||||
This method is preferred over using get_row when you do not actually need to
|
This method is preferred over using get_row when you do not actually need to
|
||||||
access any values of the row as it will not construct a full model but instead
|
access any values of the row as it will not construct a full model but instead
|
||||||
do a much more effecient query to check only if the row exists or not.
|
do a much more efficient query to check only if the row exists or not.
|
||||||
|
|
||||||
:param user: The user of whose behalf the row is being checked.
|
:param user: The user of whose behalf the row is being checked.
|
||||||
:type user: User
|
:type user: User
|
||||||
|
|
|
@ -101,6 +101,17 @@ class AggregationTypeAlreadyRegistered(Exception):
|
||||||
"""Raised when trying to register an aggregation type that exists already."""
|
"""Raised when trying to register an aggregation type that exists already."""
|
||||||
|
|
||||||
|
|
||||||
|
class DecoratorValueProviderTypeDoesNotExist(Exception):
|
||||||
|
"""Raised when trying to get a decorator value provider type that does not exist."""
|
||||||
|
|
||||||
|
|
||||||
|
class DecoratorValueProviderTypeAlreadyRegistered(Exception):
|
||||||
|
"""
|
||||||
|
Raised when trying to register a decorator value provider type that exists
|
||||||
|
already.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
class GridViewAggregationDoesNotSupportField(Exception):
|
class GridViewAggregationDoesNotSupportField(Exception):
|
||||||
"""
|
"""
|
||||||
Raised when someone tries to use an aggregation type that doesn't support the
|
Raised when someone tries to use an aggregation type that doesn't support the
|
||||||
|
|
|
@ -51,6 +51,7 @@ from .registries import (
|
||||||
view_type_registry,
|
view_type_registry,
|
||||||
view_filter_type_registry,
|
view_filter_type_registry,
|
||||||
view_aggregation_type_registry,
|
view_aggregation_type_registry,
|
||||||
|
decorator_value_provider_type_registry,
|
||||||
)
|
)
|
||||||
from .signals import (
|
from .signals import (
|
||||||
view_created,
|
view_created,
|
||||||
|
@ -359,6 +360,11 @@ class ViewHandler:
|
||||||
for view_type in view_type_registry.get_all():
|
for view_type in view_type_registry.get_all():
|
||||||
view_type.after_field_type_change(field)
|
view_type.after_field_type_change(field)
|
||||||
|
|
||||||
|
for (
|
||||||
|
decorator_value_provider_type
|
||||||
|
) in decorator_value_provider_type_registry.get_all():
|
||||||
|
decorator_value_provider_type.after_field_type_change(field)
|
||||||
|
|
||||||
def field_value_updated(self, updated_fields: Union[Iterable[Field], Field]):
|
def field_value_updated(self, updated_fields: Union[Iterable[Field], Field]):
|
||||||
"""
|
"""
|
||||||
Called after a field value has been modified because of a row creation,
|
Called after a field value has been modified because of a row creation,
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
from typing import TYPE_CHECKING, Callable, Union, List, Iterable, Tuple
|
from typing import TYPE_CHECKING, Callable, Union, List, Iterable, Tuple, Dict, Any
|
||||||
|
|
||||||
from django.contrib.auth.models import User as DjangoUser
|
from django.contrib.auth.models import User as DjangoUser
|
||||||
from django.db import models as django_models
|
from django.db import models as django_models
|
||||||
|
@ -21,9 +21,6 @@ from baserow.core.registry import (
|
||||||
)
|
)
|
||||||
from baserow.contrib.database.fields import models as field_models
|
from baserow.contrib.database.fields import models as field_models
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
|
||||||
from baserow.contrib.database.views.models import View
|
|
||||||
|
|
||||||
from .exceptions import (
|
from .exceptions import (
|
||||||
ViewTypeAlreadyRegistered,
|
ViewTypeAlreadyRegistered,
|
||||||
ViewTypeDoesNotExist,
|
ViewTypeDoesNotExist,
|
||||||
|
@ -31,8 +28,13 @@ from .exceptions import (
|
||||||
ViewFilterTypeDoesNotExist,
|
ViewFilterTypeDoesNotExist,
|
||||||
AggregationTypeDoesNotExist,
|
AggregationTypeDoesNotExist,
|
||||||
AggregationTypeAlreadyRegistered,
|
AggregationTypeAlreadyRegistered,
|
||||||
|
DecoratorValueProviderTypeAlreadyRegistered,
|
||||||
|
DecoratorValueProviderTypeDoesNotExist,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
if TYPE_CHECKING:
|
||||||
|
from baserow.contrib.database.views.models import View
|
||||||
|
|
||||||
|
|
||||||
class ViewType(
|
class ViewType(
|
||||||
MapAPIExceptionsInstanceMixin,
|
MapAPIExceptionsInstanceMixin,
|
||||||
|
@ -285,30 +287,25 @@ class ViewType(
|
||||||
id_mapping["database_view_sortings"][view_sort_id] = view_sort_object.id
|
id_mapping["database_view_sortings"][view_sort_id] = view_sort_object.id
|
||||||
|
|
||||||
if self.can_decorate:
|
if self.can_decorate:
|
||||||
|
|
||||||
def _update_field_id(node):
|
|
||||||
"""Update field ids deeply inside a deep object."""
|
|
||||||
|
|
||||||
if isinstance(node, list):
|
|
||||||
return [_update_field_id(subnode) for subnode in node]
|
|
||||||
if isinstance(node, dict):
|
|
||||||
res = {}
|
|
||||||
for key, value in node.items():
|
|
||||||
if key in ["field_id", "field"] and isinstance(value, int):
|
|
||||||
res[key] = id_mapping["database_fields"][value]
|
|
||||||
else:
|
|
||||||
res[key] = _update_field_id(value)
|
|
||||||
return res
|
|
||||||
return node
|
|
||||||
|
|
||||||
for view_decoration in decorations:
|
for view_decoration in decorations:
|
||||||
view_decoration_copy = view_decoration.copy()
|
view_decoration_copy = view_decoration.copy()
|
||||||
view_decoration_id = view_decoration_copy.pop("id")
|
view_decoration_id = view_decoration_copy.pop("id")
|
||||||
|
|
||||||
# Deeply update field ids to new one in value provider conf
|
if view_decoration["value_provider_type"]:
|
||||||
view_decoration_copy["value_provider_conf"] = _update_field_id(
|
try:
|
||||||
view_decoration_copy["value_provider_conf"]
|
value_provider_type = (
|
||||||
)
|
decorator_value_provider_type_registry.get(
|
||||||
|
view_decoration["value_provider_type"]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
except DecoratorValueProviderTypeDoesNotExist:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
view_decoration_copy = (
|
||||||
|
value_provider_type.set_import_serialized_value(
|
||||||
|
view_decoration_copy, id_mapping
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
view_decoration_object = ViewDecoration.objects.create(
|
view_decoration_object = ViewDecoration.objects.create(
|
||||||
view=view, **view_decoration_copy
|
view=view, **view_decoration_copy
|
||||||
|
@ -720,8 +717,57 @@ class ViewAggregationTypeRegistry(Registry):
|
||||||
already_registered_exception_class = AggregationTypeAlreadyRegistered
|
already_registered_exception_class = AggregationTypeAlreadyRegistered
|
||||||
|
|
||||||
|
|
||||||
|
class DecoratorValueProviderType(Instance):
|
||||||
|
"""
|
||||||
|
By declaring a new `DecoratorValueProviderType` you can define hooks on events that
|
||||||
|
can affect the decoration value provider configuration.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def set_import_serialized_value(
|
||||||
|
self, value: Dict[str, Any], id_mapping: Dict[str, Dict[int, Any]]
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""
|
||||||
|
This method is called before a decorator is imported. It can optionally be
|
||||||
|
modified. If the value_provider_conf for example points to a field or select
|
||||||
|
option id, it can be replaced with the correct value by doing a lookup in the
|
||||||
|
id_mapping.
|
||||||
|
|
||||||
|
:param value: The original exported value.
|
||||||
|
:param id_mapping: The map of exported ids to newly created ids that must be
|
||||||
|
updated when a new instance has been created.
|
||||||
|
:return: The new value that will be imported.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def after_field_delete(self, deleted_field: field_models.Field):
|
||||||
|
"""
|
||||||
|
Triggered after a field has been deleted.
|
||||||
|
This hook gives the opportunity to react when a field is deleted.
|
||||||
|
|
||||||
|
:param deleted_field: the deleted field.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def after_field_type_change(self, field: field_models.Field):
|
||||||
|
"""
|
||||||
|
This hook is called after the type of a field has changed and gives the
|
||||||
|
possibility to check compatibility or update configuration.
|
||||||
|
|
||||||
|
:param field: The concerned field.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class DecoratorValueProviderTypeRegistry(Registry):
|
||||||
|
"""
|
||||||
|
This registry contains declared decorator value provider if needed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
name = "decorator_value_provider_type"
|
||||||
|
does_not_exist_exception_class = DecoratorValueProviderTypeDoesNotExist
|
||||||
|
already_registered_exception_class = DecoratorValueProviderTypeAlreadyRegistered
|
||||||
|
|
||||||
|
|
||||||
# A default view type registry is created here, this is the one that is used
|
# A default view type registry is created here, this is the one that is used
|
||||||
# throughout the whole Baserow application to add a new view type.
|
# throughout the whole Baserow application to add a new view type.
|
||||||
view_type_registry = ViewTypeRegistry()
|
view_type_registry = ViewTypeRegistry()
|
||||||
view_filter_type_registry = ViewFilterTypeRegistry()
|
view_filter_type_registry = ViewFilterTypeRegistry()
|
||||||
view_aggregation_type_registry = ViewAggregationTypeRegistry()
|
view_aggregation_type_registry = ViewAggregationTypeRegistry()
|
||||||
|
decorator_value_provider_type_registry = DecoratorValueProviderTypeRegistry()
|
||||||
|
|
|
@ -32,3 +32,13 @@ def field_deleted(sender, field, **kwargs):
|
||||||
GalleryView.objects.filter(card_cover_image_field_id=field.id).update(
|
GalleryView.objects.filter(card_cover_image_field_id=field.id).update(
|
||||||
card_cover_image_field_id=None
|
card_cover_image_field_id=None
|
||||||
)
|
)
|
||||||
|
|
||||||
|
from baserow.contrib.database.views.registries import (
|
||||||
|
decorator_value_provider_type_registry,
|
||||||
|
)
|
||||||
|
|
||||||
|
# Call value provider type hooks
|
||||||
|
for (
|
||||||
|
decorator_value_provider_type
|
||||||
|
) in decorator_value_provider_type_registry.get_all():
|
||||||
|
decorator_value_provider_type.after_field_delete(field)
|
||||||
|
|
|
@ -1550,3 +1550,170 @@ def test_list_rows_returns_https_next_url(api_client, data_fixture, settings):
|
||||||
response_json["next"] == "https://testserver:80/api/database/rows/table/"
|
response_json["next"] == "https://testserver:80/api/database/rows/table/"
|
||||||
f"{table.id}/?page=2"
|
f"{table.id}/?page=2"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_list_row_names(api_client, data_fixture):
|
||||||
|
user, jwt_token = data_fixture.create_user_and_token(
|
||||||
|
email="test@test.nl", password="password", first_name="Test1"
|
||||||
|
)
|
||||||
|
table = data_fixture.create_database_table(user=user)
|
||||||
|
data_fixture.create_text_field(name="Name", table=table, primary=True)
|
||||||
|
|
||||||
|
# A table for another user
|
||||||
|
table_off = data_fixture.create_database_table()
|
||||||
|
|
||||||
|
# A table in the same database
|
||||||
|
table_2 = data_fixture.create_database_table(user=user, database=table.database)
|
||||||
|
data_fixture.create_text_field(name="Name", table=table_2, primary=True)
|
||||||
|
|
||||||
|
# A table in another database
|
||||||
|
table_3 = data_fixture.create_database_table(user=user)
|
||||||
|
data_fixture.create_text_field(name="Name", table=table_3, primary=True)
|
||||||
|
|
||||||
|
token = TokenHandler().create_token(user, table.database.group, "Good")
|
||||||
|
wrong_token = TokenHandler().create_token(user, table.database.group, "Wrong")
|
||||||
|
TokenHandler().update_token_permissions(user, wrong_token, True, False, True, True)
|
||||||
|
|
||||||
|
model = table.get_model(attribute_names=True)
|
||||||
|
model.objects.create(name="Alpha")
|
||||||
|
model.objects.create(name="Beta")
|
||||||
|
model.objects.create(name="Gamma")
|
||||||
|
model.objects.create(name="Omega")
|
||||||
|
|
||||||
|
model_2 = table_2.get_model(attribute_names=True)
|
||||||
|
model_2.objects.create(name="Monday")
|
||||||
|
model_2.objects.create(name="Tuesday")
|
||||||
|
|
||||||
|
model_3 = table_3.get_model(attribute_names=True)
|
||||||
|
model_3.objects.create(name="January")
|
||||||
|
|
||||||
|
url = reverse("api:database:rows:names")
|
||||||
|
response = api_client.get(
|
||||||
|
f"{url}?table__99999=1,2,3",
|
||||||
|
format="json",
|
||||||
|
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_404_NOT_FOUND
|
||||||
|
assert response.json()["error"] == "ERROR_TABLE_DOES_NOT_EXIST"
|
||||||
|
|
||||||
|
response = api_client.get(
|
||||||
|
f"{url}?table__{table.id}=1,2,3&table__99999=1,2,3",
|
||||||
|
format="json",
|
||||||
|
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_404_NOT_FOUND
|
||||||
|
assert response.json()["error"] == "ERROR_TABLE_DOES_NOT_EXIST"
|
||||||
|
|
||||||
|
response = api_client.get(
|
||||||
|
f"{url}?table__{table_off.id}=1,2,3",
|
||||||
|
format="json",
|
||||||
|
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||||
|
assert response.json()["error"] == "ERROR_USER_NOT_IN_GROUP"
|
||||||
|
|
||||||
|
response = api_client.get(
|
||||||
|
f"{url}?table__{table.id}=1,2,3",
|
||||||
|
format="json",
|
||||||
|
HTTP_AUTHORIZATION="Token abc123",
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_401_UNAUTHORIZED
|
||||||
|
assert response.json()["error"] == "ERROR_TOKEN_DOES_NOT_EXIST"
|
||||||
|
|
||||||
|
response = api_client.get(
|
||||||
|
f"{url}?table__{table.id}=1,2,3",
|
||||||
|
format="json",
|
||||||
|
HTTP_AUTHORIZATION=f"Token {wrong_token.key}",
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_401_UNAUTHORIZED
|
||||||
|
assert response.json()["error"] == "ERROR_NO_PERMISSION_TO_TABLE"
|
||||||
|
|
||||||
|
user.is_active = False
|
||||||
|
user.save()
|
||||||
|
response = api_client.get(
|
||||||
|
f"{url}?table__{table.id}=1,2,3",
|
||||||
|
format="json",
|
||||||
|
HTTP_AUTHORIZATION=f"Token {token.key}",
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_401_UNAUTHORIZED
|
||||||
|
assert response.json()["error"] == "ERROR_USER_NOT_ACTIVE"
|
||||||
|
user.is_active = True
|
||||||
|
user.save()
|
||||||
|
|
||||||
|
response = api_client.get(
|
||||||
|
f"{url}?tabble__{table.id}=1,2,3",
|
||||||
|
format="json",
|
||||||
|
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||||
|
response_json = response.json()
|
||||||
|
assert response.json()["error"] == "ERROR_QUERY_PARAMETER_VALIDATION"
|
||||||
|
assert (
|
||||||
|
response.json()["detail"]
|
||||||
|
== 'Only table Id prefixed by "table__" are allowed as parameter.'
|
||||||
|
)
|
||||||
|
|
||||||
|
response = api_client.get(
|
||||||
|
f"{url}?table__12i=1,2,3",
|
||||||
|
format="json",
|
||||||
|
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||||
|
response_json = response.json()
|
||||||
|
assert response.json()["error"] == "ERROR_QUERY_PARAMETER_VALIDATION"
|
||||||
|
assert response.json()["detail"] == 'Failed to parse table id in "table__12i".'
|
||||||
|
|
||||||
|
response = api_client.get(
|
||||||
|
f"{url}?table__23=1p,2,3",
|
||||||
|
format="json",
|
||||||
|
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||||
|
response_json = response.json()
|
||||||
|
assert response.json()["error"] == "ERROR_QUERY_PARAMETER_VALIDATION"
|
||||||
|
assert (
|
||||||
|
response.json()["detail"]
|
||||||
|
== 'Failed to parse row ids in "1p,2,3" for "table__23" parameter.'
|
||||||
|
)
|
||||||
|
|
||||||
|
response = api_client.get(
|
||||||
|
f"{url}?table__{table.id}=1",
|
||||||
|
format="json",
|
||||||
|
HTTP_AUTHORIZATION=f"Token {token.key}",
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_200_OK
|
||||||
|
|
||||||
|
# One query one table
|
||||||
|
response = api_client.get(
|
||||||
|
f"{url}?table__{table.id}=1,2,3",
|
||||||
|
format="json",
|
||||||
|
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_200_OK
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
assert response_json == {str(table.id): {"1": "Alpha", "2": "Beta", "3": "Gamma"}}
|
||||||
|
|
||||||
|
# 2 tables, one database
|
||||||
|
response = api_client.get(
|
||||||
|
f"{url}?table__{table.id}=1,2,3&table__{table_2.id}=1,2",
|
||||||
|
format="json",
|
||||||
|
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_200_OK
|
||||||
|
response_json = response.json()
|
||||||
|
|
||||||
|
assert response_json == {
|
||||||
|
str(table.id): {"1": "Alpha", "2": "Beta", "3": "Gamma"},
|
||||||
|
str(table_2.id): {"1": "Monday", "2": "Tuesday"},
|
||||||
|
}
|
||||||
|
|
||||||
|
# Two tables, two databases
|
||||||
|
response = api_client.get(
|
||||||
|
f"{url}?table__{table.id}=1,2,3&table__{table_3.id}=1",
|
||||||
|
format="json",
|
||||||
|
HTTP_AUTHORIZATION=f"JWT {jwt_token}",
|
||||||
|
)
|
||||||
|
assert response.status_code == HTTP_404_NOT_FOUND
|
||||||
|
assert response.json()["error"] == "ERROR_TABLE_DOES_NOT_EXIST"
|
||||||
|
|
|
@ -36,14 +36,7 @@ def test_import_export_grid_view(data_fixture):
|
||||||
|
|
||||||
view_decoration = data_fixture.create_view_decoration(
|
view_decoration = data_fixture.create_view_decoration(
|
||||||
view=grid_view,
|
view=grid_view,
|
||||||
value_provider_conf={
|
value_provider_conf={"config": 12},
|
||||||
"field_id": field.id,
|
|
||||||
"other": [
|
|
||||||
{"field": field.id, "other": 1},
|
|
||||||
{"answer": 42, "field_id": field.id},
|
|
||||||
{"field": {"non_int": True}},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
|
|
||||||
id_mapping = {"database_fields": {field.id: imported_field.id}}
|
id_mapping = {"database_fields": {field.id: imported_field.id}}
|
||||||
|
@ -80,14 +73,10 @@ def test_import_export_grid_view(data_fixture):
|
||||||
view_decoration.value_provider_type
|
view_decoration.value_provider_type
|
||||||
== imported_view_decoration.value_provider_type
|
== imported_view_decoration.value_provider_type
|
||||||
)
|
)
|
||||||
assert imported_view_decoration.value_provider_conf == {
|
assert (
|
||||||
"field_id": imported_field.id,
|
imported_view_decoration.value_provider_conf
|
||||||
"other": [
|
== imported_view_decoration.value_provider_conf
|
||||||
{"field": imported_field.id, "other": 1},
|
)
|
||||||
{"answer": 42, "field_id": imported_field.id},
|
|
||||||
{"field": {"non_int": True}},
|
|
||||||
],
|
|
||||||
}
|
|
||||||
assert view_decoration.order == imported_view_decoration.order
|
assert view_decoration.order == imported_view_decoration.order
|
||||||
|
|
||||||
imported_field_options = imported_grid_view.get_field_options()
|
imported_field_options = imported_grid_view.get_field_options()
|
||||||
|
|
|
@ -9,7 +9,10 @@ class BaserowPremiumConfig(AppConfig):
|
||||||
from baserow.api.user.registries import user_data_registry
|
from baserow.api.user.registries import user_data_registry
|
||||||
from baserow.contrib.database.export.registries import table_exporter_registry
|
from baserow.contrib.database.export.registries import table_exporter_registry
|
||||||
from baserow.contrib.database.rows.registries import row_metadata_registry
|
from baserow.contrib.database.rows.registries import row_metadata_registry
|
||||||
from baserow.contrib.database.views.registries import view_type_registry
|
from baserow.contrib.database.views.registries import (
|
||||||
|
view_type_registry,
|
||||||
|
decorator_value_provider_type_registry,
|
||||||
|
)
|
||||||
|
|
||||||
from baserow_premium.row_comments.row_metadata_types import (
|
from baserow_premium.row_comments.row_metadata_types import (
|
||||||
RowCommentCountMetadataType,
|
RowCommentCountMetadataType,
|
||||||
|
@ -22,6 +25,10 @@ class BaserowPremiumConfig(AppConfig):
|
||||||
from .plugins import PremiumPlugin
|
from .plugins import PremiumPlugin
|
||||||
from .export.exporter_types import JSONTableExporter, XMLTableExporter
|
from .export.exporter_types import JSONTableExporter, XMLTableExporter
|
||||||
from .views.view_types import KanbanViewType
|
from .views.view_types import KanbanViewType
|
||||||
|
from .views.decorator_value_provider_types import (
|
||||||
|
ConditionalColorValueProviderType,
|
||||||
|
SelectColorValueProviderType,
|
||||||
|
)
|
||||||
|
|
||||||
plugin_registry.register(PremiumPlugin())
|
plugin_registry.register(PremiumPlugin())
|
||||||
|
|
||||||
|
@ -34,6 +41,11 @@ class BaserowPremiumConfig(AppConfig):
|
||||||
|
|
||||||
view_type_registry.register(KanbanViewType())
|
view_type_registry.register(KanbanViewType())
|
||||||
|
|
||||||
|
decorator_value_provider_type_registry.register(
|
||||||
|
ConditionalColorValueProviderType()
|
||||||
|
)
|
||||||
|
decorator_value_provider_type_registry.register(SelectColorValueProviderType())
|
||||||
|
|
||||||
# The signals must always be imported last because they use the registries
|
# The signals must always be imported last because they use the registries
|
||||||
# which need to be filled first.
|
# which need to be filled first.
|
||||||
import baserow_premium.ws.signals # noqa: F403, F401
|
import baserow_premium.ws.signals # noqa: F403, F401
|
||||||
|
|
|
@ -0,0 +1,159 @@
|
||||||
|
from baserow.contrib.database.fields.field_types import (
|
||||||
|
SingleSelectFieldType,
|
||||||
|
)
|
||||||
|
|
||||||
|
from baserow.contrib.database.views.models import ViewDecoration
|
||||||
|
from baserow.contrib.database.views.handler import ViewHandler
|
||||||
|
from baserow.contrib.database.views.registries import (
|
||||||
|
DecoratorValueProviderType,
|
||||||
|
view_filter_type_registry,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class SelectColorValueProviderType(DecoratorValueProviderType):
|
||||||
|
type = "single_select_color"
|
||||||
|
|
||||||
|
def set_import_serialized_value(self, value, id_mapping) -> str:
|
||||||
|
"""
|
||||||
|
Update the field id with the newly created one.
|
||||||
|
"""
|
||||||
|
old_field_id = value["value_provider_conf"].get("field_id", None)
|
||||||
|
|
||||||
|
if old_field_id:
|
||||||
|
value["value_provider_conf"]["field_id"] = id_mapping[
|
||||||
|
"database_fields"
|
||||||
|
].get(old_field_id, None)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def after_field_delete(self, deleted_field):
|
||||||
|
"""
|
||||||
|
Remove the field from the value_provider_conf filters if necessary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
view_handler = ViewHandler()
|
||||||
|
|
||||||
|
queryset = ViewDecoration.objects.filter(
|
||||||
|
view__table=deleted_field.table,
|
||||||
|
value_provider_type=SelectColorValueProviderType.type,
|
||||||
|
value_provider_conf__field_id=deleted_field.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
for decoration in queryset.all():
|
||||||
|
new_conf = {**decoration.value_provider_conf}
|
||||||
|
new_conf["field_id"] = None
|
||||||
|
view_handler.update_decoration(decoration, value_provider_conf=new_conf)
|
||||||
|
|
||||||
|
def after_field_type_change(self, field):
|
||||||
|
"""
|
||||||
|
Unset the field if the type is not a select anymore.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from baserow.contrib.database.fields.registries import field_type_registry
|
||||||
|
|
||||||
|
field_type = field_type_registry.get_by_model(field.specific_class)
|
||||||
|
|
||||||
|
view_handler = ViewHandler()
|
||||||
|
|
||||||
|
if field_type.type != SingleSelectFieldType.type:
|
||||||
|
|
||||||
|
queryset = ViewDecoration.objects.filter(
|
||||||
|
view__table=field.table,
|
||||||
|
value_provider_type=SelectColorValueProviderType.type,
|
||||||
|
value_provider_conf__field_id=field.id,
|
||||||
|
)
|
||||||
|
|
||||||
|
for decoration in queryset.all():
|
||||||
|
new_conf = {**decoration.value_provider_conf}
|
||||||
|
new_conf["field_id"] = None
|
||||||
|
view_handler.update_decoration(decoration, value_provider_conf=new_conf)
|
||||||
|
|
||||||
|
|
||||||
|
class ConditionalColorValueProviderType(DecoratorValueProviderType):
|
||||||
|
type = "conditional_color"
|
||||||
|
|
||||||
|
def set_import_serialized_value(self, value, id_mapping) -> str:
|
||||||
|
"""
|
||||||
|
Update the field ids of each filter with the newly created one.
|
||||||
|
"""
|
||||||
|
|
||||||
|
value_provider_conf = value["value_provider_conf"]
|
||||||
|
|
||||||
|
for color in value_provider_conf["colors"]:
|
||||||
|
for filter in color["filters"]:
|
||||||
|
new_field_id = id_mapping["database_fields"][filter["field"]]
|
||||||
|
filter["field"] = new_field_id
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
def _map_filter_from_config(self, conf, fn):
|
||||||
|
"""
|
||||||
|
Map a function on each filters of the configuration. If the given function
|
||||||
|
returns None, the filter is removed from the list of filters.
|
||||||
|
"""
|
||||||
|
|
||||||
|
modified = False
|
||||||
|
for color in conf["colors"]:
|
||||||
|
new_filters = []
|
||||||
|
for filter in color["filters"]:
|
||||||
|
new_filter = fn(filter)
|
||||||
|
modified = modified or new_filter != filter
|
||||||
|
if new_filter is not None:
|
||||||
|
new_filters.append(new_filter)
|
||||||
|
|
||||||
|
modified = modified or len(new_filters) != len(color["filters"])
|
||||||
|
color["filters"] = new_filters
|
||||||
|
|
||||||
|
return conf, modified
|
||||||
|
|
||||||
|
def after_field_delete(self, deleted_field):
|
||||||
|
"""
|
||||||
|
Remove the field from the value_provider_conf filters if necessary.
|
||||||
|
"""
|
||||||
|
|
||||||
|
# We can't filter with the JSON field here as we have nested lists
|
||||||
|
queryset = ViewDecoration.objects.filter(
|
||||||
|
view__table=deleted_field.table,
|
||||||
|
value_provider_type=ConditionalColorValueProviderType.type,
|
||||||
|
)
|
||||||
|
|
||||||
|
view_handler = ViewHandler()
|
||||||
|
|
||||||
|
for decoration in queryset.all():
|
||||||
|
new_conf, modified = self._map_filter_from_config(
|
||||||
|
decoration.value_provider_conf,
|
||||||
|
lambda f: None if f["field"] == deleted_field.id else f,
|
||||||
|
)
|
||||||
|
if modified:
|
||||||
|
view_handler.update_decoration(decoration, value_provider_conf=new_conf)
|
||||||
|
|
||||||
|
def after_field_type_change(self, field):
|
||||||
|
"""
|
||||||
|
Remove filters type that are not compatible anymore from configuration
|
||||||
|
"""
|
||||||
|
|
||||||
|
queryset = ViewDecoration.objects.filter(
|
||||||
|
view__table=field.table,
|
||||||
|
value_provider_type=ConditionalColorValueProviderType.type,
|
||||||
|
)
|
||||||
|
|
||||||
|
view_handler = ViewHandler()
|
||||||
|
|
||||||
|
def compatible_filter_only(filter):
|
||||||
|
if filter["field"] != field.id:
|
||||||
|
return filter
|
||||||
|
|
||||||
|
filter_type = view_filter_type_registry.get(filter["type"])
|
||||||
|
if not filter_type.field_is_compatible(field):
|
||||||
|
return None
|
||||||
|
return filter
|
||||||
|
|
||||||
|
for decoration in queryset.all():
|
||||||
|
# Check which filters are not compatible anymore and remove those.
|
||||||
|
new_conf, modified = self._map_filter_from_config(
|
||||||
|
decoration.value_provider_conf,
|
||||||
|
compatible_filter_only,
|
||||||
|
)
|
||||||
|
|
||||||
|
if modified:
|
||||||
|
view_handler.update_decoration(decoration, value_provider_conf=new_conf)
|
|
@ -1,7 +1,8 @@
|
||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
from baserow.contrib.database.views.models import View
|
from baserow.contrib.database.views.models import View, ViewDecoration
|
||||||
from baserow_premium.views.handler import get_rows_grouped_by_single_select_field
|
from baserow_premium.views.handler import get_rows_grouped_by_single_select_field
|
||||||
|
from baserow.contrib.database.fields.handler import FieldHandler
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
@ -232,3 +233,149 @@ def test_get_rows_grouped_by_single_select_field_with_empty_table(
|
||||||
assert len(rows) == 1
|
assert len(rows) == 1
|
||||||
assert rows["null"]["count"] == 0
|
assert rows["null"]["count"] == 0
|
||||||
assert len(rows["null"]["results"]) == 0
|
assert len(rows["null"]["results"]) == 0
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_field_type_changed_w_decoration(data_fixture):
|
||||||
|
user = data_fixture.create_user()
|
||||||
|
table = data_fixture.create_database_table(user=user)
|
||||||
|
text_field = data_fixture.create_text_field(table=table)
|
||||||
|
option_field = data_fixture.create_single_select_field(
|
||||||
|
table=table, name="option_field", order=1
|
||||||
|
)
|
||||||
|
option_a = data_fixture.create_select_option(
|
||||||
|
field=option_field, value="A", color="blue"
|
||||||
|
)
|
||||||
|
|
||||||
|
grid_view = data_fixture.create_grid_view(table=table)
|
||||||
|
|
||||||
|
select_view_decoration = data_fixture.create_view_decoration(
|
||||||
|
view=grid_view,
|
||||||
|
value_provider_type="single_select_color",
|
||||||
|
value_provider_conf={"field_id": option_field.id},
|
||||||
|
order=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
condition_view_decoration = data_fixture.create_view_decoration(
|
||||||
|
view=grid_view,
|
||||||
|
value_provider_type="conditional_color",
|
||||||
|
value_provider_conf={
|
||||||
|
"colors": [
|
||||||
|
{"filters": [{"field": text_field.id, "type": "equal"}]},
|
||||||
|
{"filters": [{"field": option_field.id, "type": "equal"}]},
|
||||||
|
{
|
||||||
|
"filters": [
|
||||||
|
{"field": option_field.id, "type": "single_select_equal"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{"filters": []},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
order=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
field_handler = FieldHandler()
|
||||||
|
|
||||||
|
decorations = list(ViewDecoration.objects.all())
|
||||||
|
assert len(decorations) == 2
|
||||||
|
assert (
|
||||||
|
decorations[0].value_provider_conf == select_view_decoration.value_provider_conf
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
decorations[1].value_provider_conf
|
||||||
|
== condition_view_decoration.value_provider_conf
|
||||||
|
)
|
||||||
|
|
||||||
|
field_handler.update_field(
|
||||||
|
user=user, field=option_field, new_type_name="single_select"
|
||||||
|
)
|
||||||
|
|
||||||
|
decorations = list(ViewDecoration.objects.all())
|
||||||
|
assert (
|
||||||
|
decorations[0].value_provider_conf == select_view_decoration.value_provider_conf
|
||||||
|
)
|
||||||
|
assert (
|
||||||
|
decorations[1].value_provider_conf
|
||||||
|
== condition_view_decoration.value_provider_conf
|
||||||
|
)
|
||||||
|
|
||||||
|
field_handler.update_field(user=user, field=option_field, new_type_name="text")
|
||||||
|
|
||||||
|
decorations = list(ViewDecoration.objects.all())
|
||||||
|
assert decorations[0].value_provider_conf == {"field_id": None}
|
||||||
|
assert decorations[1].value_provider_conf == {
|
||||||
|
"colors": [
|
||||||
|
{"filters": [{"type": "equal", "field": text_field.id}]},
|
||||||
|
{"filters": [{"type": "equal", "field": option_field.id}]},
|
||||||
|
{"filters": []},
|
||||||
|
{"filters": []},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_field_deleted_w_decoration(data_fixture):
|
||||||
|
user = data_fixture.create_user()
|
||||||
|
table = data_fixture.create_database_table(user=user)
|
||||||
|
text_field = data_fixture.create_text_field(table=table)
|
||||||
|
option_field = data_fixture.create_single_select_field(
|
||||||
|
table=table, name="option_field", order=1
|
||||||
|
)
|
||||||
|
option_a = data_fixture.create_select_option(
|
||||||
|
field=option_field, value="A", color="blue"
|
||||||
|
)
|
||||||
|
|
||||||
|
grid_view = data_fixture.create_grid_view(table=table)
|
||||||
|
|
||||||
|
select_view_decoration = data_fixture.create_view_decoration(
|
||||||
|
view=grid_view,
|
||||||
|
value_provider_type="single_select_color",
|
||||||
|
value_provider_conf={"field_id": option_field.id},
|
||||||
|
order=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
condition_view_decoration = data_fixture.create_view_decoration(
|
||||||
|
view=grid_view,
|
||||||
|
value_provider_type="conditional_color",
|
||||||
|
value_provider_conf={
|
||||||
|
"colors": [
|
||||||
|
{"filters": [{"field": text_field.id, "type": "equal"}]},
|
||||||
|
{"filters": [{"field": option_field.id, "type": "equal"}]},
|
||||||
|
{
|
||||||
|
"filters": [
|
||||||
|
{"field": option_field.id, "type": "single_select_equal"}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{"filters": []},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
order=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
field_handler = FieldHandler()
|
||||||
|
|
||||||
|
field_handler.delete_field(user=user, field=option_field)
|
||||||
|
|
||||||
|
decorations = list(ViewDecoration.objects.all())
|
||||||
|
assert decorations[0].value_provider_conf == {"field_id": None}
|
||||||
|
assert decorations[1].value_provider_conf == {
|
||||||
|
"colors": [
|
||||||
|
{"filters": [{"type": "equal", "field": text_field.id}]},
|
||||||
|
{"filters": []},
|
||||||
|
{"filters": []},
|
||||||
|
{"filters": []},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
field_handler.delete_field(user=user, field=text_field)
|
||||||
|
|
||||||
|
decorations = list(ViewDecoration.objects.all())
|
||||||
|
assert decorations[0].value_provider_conf == {"field_id": None}
|
||||||
|
assert decorations[1].value_provider_conf == {
|
||||||
|
"colors": [
|
||||||
|
{"filters": []},
|
||||||
|
{"filters": []},
|
||||||
|
{"filters": []},
|
||||||
|
{"filters": []},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
|
@ -130,6 +130,66 @@ def test_import_export_kanban_view(premium_data_fixture, tmpdir):
|
||||||
assert field_option.order == imported_field_option.order
|
assert field_option.order == imported_field_option.order
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_import_export_grid_view_w_decorator(data_fixture):
|
||||||
|
grid_view = data_fixture.create_grid_view(
|
||||||
|
name="Test", order=1, filter_type="AND", filters_disabled=False
|
||||||
|
)
|
||||||
|
field = data_fixture.create_text_field(table=grid_view.table)
|
||||||
|
imported_field = data_fixture.create_text_field(table=grid_view.table)
|
||||||
|
|
||||||
|
view_decoration = data_fixture.create_view_decoration(
|
||||||
|
view=grid_view,
|
||||||
|
value_provider_type="single_select_color",
|
||||||
|
value_provider_conf={"field_id": field.id},
|
||||||
|
order=1,
|
||||||
|
)
|
||||||
|
|
||||||
|
view_decoration_2 = data_fixture.create_view_decoration(
|
||||||
|
view=grid_view,
|
||||||
|
value_provider_type="conditional_color",
|
||||||
|
value_provider_conf={
|
||||||
|
"colors": [
|
||||||
|
{"filters": [{"field": field.id}]},
|
||||||
|
{"filters": [{"field": field.id}]},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
order=2,
|
||||||
|
)
|
||||||
|
|
||||||
|
id_mapping = {"database_fields": {field.id: imported_field.id}}
|
||||||
|
|
||||||
|
grid_view_type = view_type_registry.get("grid")
|
||||||
|
serialized = grid_view_type.export_serialized(grid_view, None, None)
|
||||||
|
imported_grid_view = grid_view_type.import_serialized(
|
||||||
|
grid_view.table, serialized, id_mapping, None, None
|
||||||
|
)
|
||||||
|
|
||||||
|
imported_view_decorations = imported_grid_view.viewdecoration_set.all()
|
||||||
|
assert view_decoration.id != imported_view_decorations[0].id
|
||||||
|
assert view_decoration.type == imported_view_decorations[0].type
|
||||||
|
assert (
|
||||||
|
view_decoration.value_provider_type
|
||||||
|
== imported_view_decorations[0].value_provider_type
|
||||||
|
)
|
||||||
|
assert imported_view_decorations[0].value_provider_conf == {
|
||||||
|
"field_id": imported_field.id
|
||||||
|
}
|
||||||
|
|
||||||
|
assert view_decoration_2.id != imported_view_decorations[1].id
|
||||||
|
assert view_decoration_2.type == imported_view_decorations[1].type
|
||||||
|
assert (
|
||||||
|
view_decoration_2.value_provider_type
|
||||||
|
== imported_view_decorations[1].value_provider_type
|
||||||
|
)
|
||||||
|
assert imported_view_decorations[1].value_provider_conf == {
|
||||||
|
"colors": [
|
||||||
|
{"filters": [{"field": imported_field.id}]},
|
||||||
|
{"filters": [{"field": imported_field.id}]},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_newly_created_view(premium_data_fixture):
|
def test_newly_created_view(premium_data_fixture):
|
||||||
user = premium_data_fixture.create_user(has_active_premium_license=True)
|
user = premium_data_fixture.create_user(has_active_premium_license=True)
|
||||||
|
|
|
@ -9,3 +9,4 @@
|
||||||
@import 'views/kanban';
|
@import 'views/kanban';
|
||||||
@import 'views/decorators';
|
@import 'views/decorators';
|
||||||
@import 'impersonate_warning';
|
@import 'impersonate_warning';
|
||||||
|
@import 'views/conditional_color_value_provider_form';
|
||||||
|
|
|
@ -0,0 +1,65 @@
|
||||||
|
.conditional-color-value-provider-form__color {
|
||||||
|
background-color: $color-neutral-100;
|
||||||
|
padding: 12px 30px;
|
||||||
|
margin: 0 -20px;
|
||||||
|
margin-bottom: 12px;
|
||||||
|
|
||||||
|
& .filters__item {
|
||||||
|
margin-left: 1px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.conditional-color-value-provider-form__color-header {
|
||||||
|
display: flex;
|
||||||
|
justify-content: left;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conditional-color-value-provider-form__color-handle {
|
||||||
|
width: 12px;
|
||||||
|
height: 24px;
|
||||||
|
background-image: radial-gradient($color-neutral-200 40%, transparent 40%);
|
||||||
|
background-size: 4px 4px;
|
||||||
|
background-repeat: repeat;
|
||||||
|
margin-right: 12px;
|
||||||
|
cursor: grab;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conditional-color-value-provider-form__color-trash-link {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
color: $color-primary-900;
|
||||||
|
padding: 6px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-left: 4px;
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
text-decoration: none;
|
||||||
|
background-color: $color-neutral-100;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.conditional-color-value-provider-form__color-color {
|
||||||
|
@extend %option-shadow;
|
||||||
|
|
||||||
|
padding: 6px 9px;
|
||||||
|
text-align: center;
|
||||||
|
color: $color-primary-900;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conditional-color-value-provider-form__color-filter--empty {
|
||||||
|
padding: 12px;
|
||||||
|
white-space: normal;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conditional-color-value-provider-form__color-filters {
|
||||||
|
margin-bottom: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.conditional-color-value-provider-form__color-filter-add {
|
||||||
|
color: $color-primary-900;
|
||||||
|
}
|
|
@ -1,11 +1,87 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="margin-bottom-2">Not available yet!</div>
|
<div>
|
||||||
|
<div>
|
||||||
|
<div
|
||||||
|
v-for="color in options.colors || []"
|
||||||
|
:key="color.uid"
|
||||||
|
v-sortable="{
|
||||||
|
id: color.uid,
|
||||||
|
update: orderColor,
|
||||||
|
handle: '[data-sortable-handle]',
|
||||||
|
marginTop: -5,
|
||||||
|
}"
|
||||||
|
class="conditional-color-value-provider-form__color"
|
||||||
|
>
|
||||||
|
<div class="conditional-color-value-provider-form__color-header">
|
||||||
|
<div
|
||||||
|
class="conditional-color-value-provider-form__color-handle"
|
||||||
|
data-sortable-handle
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
:ref="`colorSelect-${color.uid}`"
|
||||||
|
class="conditional-color-value-provider-form__color-color"
|
||||||
|
:class="`background-color--${color.color}`"
|
||||||
|
@click="openColor(color)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-caret-down"></i>
|
||||||
|
</a>
|
||||||
|
<div :style="{ flex: 1 }" />
|
||||||
|
<a
|
||||||
|
v-if="options.colors.length > 1"
|
||||||
|
class="conditional-color-value-provider-form__color-trash-link"
|
||||||
|
@click="deleteColor(color)"
|
||||||
|
>
|
||||||
|
<i class="fa fa-trash" />
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div
|
||||||
|
v-if="color.filters.length === 0"
|
||||||
|
class="conditional-color-value-provider-form__color-filter--empty"
|
||||||
|
>
|
||||||
|
{{ $t('conditionalColorValueProviderForm.colorAlwaysApply') }}
|
||||||
|
</div>
|
||||||
|
<ViewFieldConditionsForm
|
||||||
|
v-show="color.filters.length !== 0"
|
||||||
|
class="conditional-color-value-provider-form__color-filters"
|
||||||
|
:filters="color.filters"
|
||||||
|
:disable-filter="false"
|
||||||
|
:filter-type="color.operator"
|
||||||
|
:primary="primary"
|
||||||
|
:fields="fields"
|
||||||
|
:view="view"
|
||||||
|
:read-only="readOnly"
|
||||||
|
@deleteFilter="deleteFilter(color, $event)"
|
||||||
|
@updateFilter="updateFilter(color, $event)"
|
||||||
|
@selectOperator="updateColor(color, { operator: $event })"
|
||||||
|
/>
|
||||||
|
<a
|
||||||
|
class="conditional-color-value-provider-form__color-filter-add"
|
||||||
|
@click.prevent="addFilter(color)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
{{ $t('conditionalColorValueProviderForm.addCondition') }}</a
|
||||||
|
>
|
||||||
|
<ColorSelectContext
|
||||||
|
:ref="`colorContext-${color.uid}`"
|
||||||
|
@selected="updateColor(color, { color: $event })"
|
||||||
|
></ColorSelectContext>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<a class="colors__add" @click.prevent="addColor()">
|
||||||
|
<i class="fas fa-plus"></i>
|
||||||
|
{{ $t('conditionalColorValueProviderForm.addColor') }}</a
|
||||||
|
>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
import ViewFieldConditionsForm from '@baserow/modules/database/components/view/ViewFieldConditionsForm'
|
||||||
|
import ColorSelectContext from '@baserow/modules/core/components/ColorSelectContext'
|
||||||
|
import { ConditionalColorValueProviderType } from '@baserow_premium/decoratorValueProviders'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ConditionalColorValueProvider',
|
name: 'ConditionalColorValueProvider',
|
||||||
components: {},
|
components: { ViewFieldConditionsForm, ColorSelectContext },
|
||||||
props: {
|
props: {
|
||||||
options: {
|
options: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -19,6 +95,10 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
primary: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
fields: {
|
fields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -28,6 +108,126 @@ export default {
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {},
|
computed: {
|
||||||
|
allFields() {
|
||||||
|
return [this.primary, ...this.fields]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
orderColor(colorIds) {
|
||||||
|
const newColors = colorIds.map((colorId) =>
|
||||||
|
this.options.colors.find(({ uid }) => uid === colorId)
|
||||||
|
)
|
||||||
|
this.$emit('update', {
|
||||||
|
colors: newColors,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
openColor(color) {
|
||||||
|
this.$refs[`colorContext-${color.uid}`][0].setActive(color.color)
|
||||||
|
this.$refs[`colorContext-${color.uid}`][0].toggle(
|
||||||
|
this.$refs[`colorSelect-${color.uid}`][0],
|
||||||
|
'bottom',
|
||||||
|
'left',
|
||||||
|
4
|
||||||
|
)
|
||||||
|
},
|
||||||
|
addColor() {
|
||||||
|
this.$emit('update', {
|
||||||
|
colors: [
|
||||||
|
...this.options.colors,
|
||||||
|
ConditionalColorValueProviderType.getDefaultColorConf(
|
||||||
|
this.$registry,
|
||||||
|
{
|
||||||
|
fields: this.allFields,
|
||||||
|
},
|
||||||
|
true
|
||||||
|
),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateColor(color, values) {
|
||||||
|
const newColors = this.options.colors.map((colorConf) => {
|
||||||
|
if (colorConf.uid === color.uid) {
|
||||||
|
return { ...colorConf, ...values }
|
||||||
|
}
|
||||||
|
return colorConf
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$emit('update', {
|
||||||
|
colors: newColors,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteColor(color) {
|
||||||
|
const newColors = this.options.colors.filter(({ uid }) => {
|
||||||
|
return uid !== color.uid
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$emit('update', {
|
||||||
|
colors: newColors,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
addFilter(color) {
|
||||||
|
const newColors = this.options.colors.map((colorConf) => {
|
||||||
|
if (colorConf.uid === color.uid) {
|
||||||
|
return {
|
||||||
|
...colorConf,
|
||||||
|
filters: [
|
||||||
|
...colorConf.filters,
|
||||||
|
ConditionalColorValueProviderType.getDefaultFilterConf(
|
||||||
|
this.$registry,
|
||||||
|
{
|
||||||
|
fields: this.allFields,
|
||||||
|
}
|
||||||
|
),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return colorConf
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$emit('update', {
|
||||||
|
colors: newColors,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
updateFilter(color, { filter, values }) {
|
||||||
|
const newColors = this.options.colors.map((colorConf) => {
|
||||||
|
if (colorConf.uid === color.uid) {
|
||||||
|
const newFilters = colorConf.filters.map((filterConf) => {
|
||||||
|
if (filterConf.id === filter.id) {
|
||||||
|
return { ...filter, ...values }
|
||||||
|
}
|
||||||
|
return filterConf
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
...colorConf,
|
||||||
|
filters: newFilters,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return colorConf
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$emit('update', {
|
||||||
|
colors: newColors,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteFilter(color, filter) {
|
||||||
|
const newColors = this.options.colors.map((colorConf) => {
|
||||||
|
if (colorConf.uid === color.uid) {
|
||||||
|
const newFilters = colorConf.filters.filter((filterConf) => {
|
||||||
|
return filterConf.id !== filter.id
|
||||||
|
})
|
||||||
|
return {
|
||||||
|
...colorConf,
|
||||||
|
filters: newFilters,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return colorConf
|
||||||
|
})
|
||||||
|
|
||||||
|
this.$emit('update', {
|
||||||
|
colors: newColors,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -15,6 +15,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import ChooseSingleSelectField from '@baserow/modules/database/components/field/ChooseSingleSelectField.vue'
|
import ChooseSingleSelectField from '@baserow/modules/database/components/field/ChooseSingleSelectField.vue'
|
||||||
|
import { SingleSelectFieldType } from '@baserow/modules/database/fieldTypes'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'SingleSelectColorValueProviderForm',
|
name: 'SingleSelectColorValueProviderForm',
|
||||||
|
@ -32,6 +33,10 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
primary: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
fields: {
|
fields: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -43,7 +48,9 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
selectFields() {
|
selectFields() {
|
||||||
return this.fields.filter(({ type }) => type === 'single_select')
|
return [this.primary, ...this.fields].filter(
|
||||||
|
({ type }) => type === SingleSelectFieldType.getType()
|
||||||
|
)
|
||||||
},
|
},
|
||||||
value() {
|
value() {
|
||||||
return this.options && this.options.field_id
|
return this.options && this.options.field_id
|
||||||
|
|
|
@ -6,6 +6,9 @@ import {
|
||||||
BackgroundColorViewDecoratorType,
|
BackgroundColorViewDecoratorType,
|
||||||
LeftBorderColorViewDecoratorType,
|
LeftBorderColorViewDecoratorType,
|
||||||
} from '@baserow_premium/viewDecorators'
|
} from '@baserow_premium/viewDecorators'
|
||||||
|
import { uuid } from '@baserow/modules/core/utils/string'
|
||||||
|
import { randomColor } from '@baserow/modules/core/utils/colors'
|
||||||
|
import { matchSearchFilters } from '@baserow/modules/database/utils/view'
|
||||||
|
|
||||||
export class SingleSelectColorValueProviderType extends DecoratorValueProviderType {
|
export class SingleSelectColorValueProviderType extends DecoratorValueProviderType {
|
||||||
static getType() {
|
static getType() {
|
||||||
|
@ -52,6 +55,41 @@ export class ConditionalColorValueProviderType extends DecoratorValueProviderTyp
|
||||||
return 'conditional_color'
|
return 'conditional_color'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getDefaultFilterConf(registry, { fields }) {
|
||||||
|
const field = fields[0]
|
||||||
|
const filter = { field: field.id }
|
||||||
|
|
||||||
|
const viewFilterTypes = registry.getAll('viewFilter')
|
||||||
|
const compatibleType = Object.values(viewFilterTypes).find(
|
||||||
|
(viewFilterType) => {
|
||||||
|
return viewFilterType.fieldIsCompatible(field)
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
filter.type = compatibleType.type
|
||||||
|
const viewFilterType = registry.get('viewFilter', filter.type)
|
||||||
|
filter.value = viewFilterType.getDefaultValue()
|
||||||
|
filter.preload_values = {}
|
||||||
|
filter.id = uuid()
|
||||||
|
|
||||||
|
return filter
|
||||||
|
}
|
||||||
|
|
||||||
|
static getDefaultColorConf(registry, { fields }, noFilter = false) {
|
||||||
|
return {
|
||||||
|
color: randomColor(),
|
||||||
|
operator: 'AND',
|
||||||
|
filters: noFilter
|
||||||
|
? []
|
||||||
|
: [
|
||||||
|
ConditionalColorValueProviderType.getDefaultFilterConf(registry, {
|
||||||
|
fields,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
uid: uuid(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
getName() {
|
getName() {
|
||||||
const { i18n } = this.app
|
const { i18n } = this.app
|
||||||
return i18n.t('decoratorValueProviderType.conditionalColor')
|
return i18n.t('decoratorValueProviderType.conditionalColor')
|
||||||
|
@ -67,6 +105,12 @@ export class ConditionalColorValueProviderType extends DecoratorValueProviderTyp
|
||||||
}
|
}
|
||||||
|
|
||||||
getValue({ options, fields, row }) {
|
getValue({ options, fields, row }) {
|
||||||
|
const { $registry } = this.app
|
||||||
|
for (const { color, filters, operator } of options.colors) {
|
||||||
|
if (matchSearchFilters($registry, operator, filters, fields, row)) {
|
||||||
|
return color
|
||||||
|
}
|
||||||
|
}
|
||||||
return ''
|
return ''
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -77,4 +121,16 @@ export class ConditionalColorValueProviderType extends DecoratorValueProviderTyp
|
||||||
getFormComponent() {
|
getFormComponent() {
|
||||||
return ConditionalColorValueProviderForm
|
return ConditionalColorValueProviderForm
|
||||||
}
|
}
|
||||||
|
|
||||||
|
getDefaultConfiguration({ fields }) {
|
||||||
|
const { $registry } = this.app
|
||||||
|
return {
|
||||||
|
default: null,
|
||||||
|
colors: [
|
||||||
|
ConditionalColorValueProviderType.getDefaultColorConf($registry, {
|
||||||
|
fields,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -237,5 +237,10 @@
|
||||||
},
|
},
|
||||||
"singleSelectColorValueProviderForm": {
|
"singleSelectColorValueProviderForm": {
|
||||||
"chooseAColor": "Which single select field should the row be colored by?"
|
"chooseAColor": "Which single select field should the row be colored by?"
|
||||||
|
},
|
||||||
|
"conditionalColorValueProviderForm": {
|
||||||
|
"addCondition": "add condition",
|
||||||
|
"colorAlwaysApply": "This color applies by default. You can add conditions by clicking on the \"Add condition\" button.",
|
||||||
|
"addColor": "add color"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
.filters {
|
.filters {
|
||||||
padding: 12px;
|
padding: 12px;
|
||||||
|
width: 550px;
|
||||||
|
|
||||||
.dropdown__selected {
|
.dropdown__selected {
|
||||||
@extend %ellipsis;
|
@extend %ellipsis;
|
||||||
|
@ -23,34 +24,33 @@
|
||||||
|
|
||||||
.filters__item {
|
.filters__item {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: flex;
|
display: grid;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
// 142px = 20 + 82 + 10 * 4 (gaps)
|
||||||
|
grid-template-columns: 20px 82px calc(50% - 142px) 22% 28%;
|
||||||
padding: 6px 0;
|
padding: 6px 0;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
margin-left: 5px;
|
||||||
|
column-gap: 10px;
|
||||||
|
|
||||||
&:not(:last-child) {
|
&:not(:last-child) {
|
||||||
margin-bottom: 6px;
|
margin-bottom: 6px;
|
||||||
}
|
}
|
||||||
|
|
||||||
&.filters__item--loading {
|
&.filters__item--loading {
|
||||||
padding-left: 32px;
|
|
||||||
|
|
||||||
&::before {
|
&::before {
|
||||||
content: '';
|
content: '';
|
||||||
margin-top: -7px;
|
margin-top: -7px;
|
||||||
|
|
||||||
@include loading(14px);
|
@include loading(14px);
|
||||||
@include absolute(50%, auto, 0, 10px);
|
@include absolute(50%, auto, 0, -2px);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters__remove {
|
.filters__remove {
|
||||||
flex: 0 0 32px;
|
|
||||||
width: 32px;
|
|
||||||
color: $color-primary-900;
|
color: $color-primary-900;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
text-align: center;
|
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
@ -58,32 +58,16 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters__item--loading & {
|
.filters__item--loading & {
|
||||||
display: none;
|
visibility: hidden;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters__operator {
|
.filters__operator {
|
||||||
flex: 0 0 72px;
|
|
||||||
width: 72px;
|
|
||||||
margin-right: 10px;
|
|
||||||
|
|
||||||
span {
|
span {
|
||||||
padding-left: 12px;
|
padding-left: 12px;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters__field {
|
|
||||||
margin-right: 10px;
|
|
||||||
|
|
||||||
@include filter-dropdown-width(100px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters__type {
|
|
||||||
margin-right: 10px;
|
|
||||||
|
|
||||||
@include filter-dropdown-width(100px);
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters__value {
|
.filters__value {
|
||||||
flex: 0 0;
|
flex: 0 0;
|
||||||
}
|
}
|
||||||
|
@ -96,7 +80,6 @@
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters__value-input {
|
.filters__value-input {
|
||||||
width: 130px;
|
|
||||||
padding-top: 0;
|
padding-top: 0;
|
||||||
padding-bottom: 0;
|
padding-bottom: 0;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
|
@ -113,10 +96,6 @@
|
||||||
color: $color-neutral-400;
|
color: $color-neutral-400;
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters__value-dropdown {
|
|
||||||
width: 130px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.filters__value-rating {
|
.filters__value-rating {
|
||||||
border: solid 1px $color-neutral-400;
|
border: solid 1px $color-neutral-400;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
|
@ -128,13 +107,13 @@
|
||||||
|
|
||||||
display: block;
|
display: block;
|
||||||
position: relative;
|
position: relative;
|
||||||
width: 130px;
|
|
||||||
color: $color-primary-900;
|
color: $color-primary-900;
|
||||||
line-height: 30px;
|
line-height: 30px;
|
||||||
height: 30px;
|
height: 30px;
|
||||||
border: solid 1px $color-neutral-400;
|
border: solid 1px $color-neutral-400;
|
||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
padding: 0 10px;
|
padding: 0 10px;
|
||||||
|
background-color: $white;
|
||||||
|
|
||||||
&:hover {
|
&:hover {
|
||||||
text-decoration: none;
|
text-decoration: none;
|
||||||
|
@ -150,6 +129,16 @@
|
||||||
border-color: $color-neutral-400;
|
border-color: $color-neutral-400;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
&.filters__value-link-row--loading {
|
||||||
|
&::before {
|
||||||
|
content: '';
|
||||||
|
margin-top: -7px;
|
||||||
|
|
||||||
|
@include loading(14px);
|
||||||
|
@include absolute(50%, auto, 0, calc(50% - 7px));
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.filters__value-link-row-choose {
|
.filters__value-link-row-choose {
|
||||||
|
|
|
@ -7,8 +7,8 @@
|
||||||
>
|
>
|
||||||
<i class="dropdown__selected-icon fas" :class="'fa-' + icon" />
|
<i class="dropdown__selected-icon fas" :class="'fa-' + icon" />
|
||||||
{{ name }}
|
{{ name }}
|
||||||
|
<i class="dropdown__toggle-icon fas fa-caret-down"></i>
|
||||||
</a>
|
</a>
|
||||||
<i class="dropdown__toggle-icon fas fa-caret-down"></i>
|
|
||||||
<Context ref="pickerContext" class="picker__context">
|
<Context ref="pickerContext" class="picker__context">
|
||||||
<slot :hidePicker="hide" />
|
<slot :hidePicker="hide" />
|
||||||
</Context>
|
</Context>
|
||||||
|
|
|
@ -16,18 +16,18 @@ import { findScrollableParent } from '@baserow/modules/core/utils/dom'
|
||||||
* <div
|
* <div
|
||||||
* v-for="item in items"
|
* v-for="item in items"
|
||||||
* :key="item.id"
|
* :key="item.id"
|
||||||
* v-sortable="{ id: item.id, update: order }"
|
* v-sortable="{ id: item.id, update: onUpdate }"
|
||||||
* ></div>
|
* ></div>
|
||||||
*
|
*
|
||||||
* export default {
|
* export default {
|
||||||
* data() {
|
* data() {
|
||||||
* return {
|
* return {
|
||||||
* items: [{'id': 1, order: 1}, {'id': 2, order: 2}, {'id': 3, order: 3}]
|
* items: [{'id': 25, order: 1}, {'id': 27, order: 2}, {'id': 30, order: 3}]
|
||||||
* }
|
* }
|
||||||
* },
|
* },
|
||||||
* methods: {
|
* methods: {
|
||||||
* order(order) {
|
* onUpdate(itemIds) {
|
||||||
* console.log(order) // [1, 2, 3]
|
* console.log(itemIds) // [25, 27, 30]
|
||||||
* },
|
* },
|
||||||
* },
|
* },
|
||||||
* }
|
* }
|
||||||
|
@ -76,6 +76,12 @@ export default {
|
||||||
|
|
||||||
parent = el.parentNode
|
parent = el.parentNode
|
||||||
scrollableParent = findScrollableParent(parent) || parent
|
scrollableParent = findScrollableParent(parent) || parent
|
||||||
|
|
||||||
|
// If the parent container is not positioned, add the position automatically.
|
||||||
|
if (getComputedStyle(parent).position === 'static') {
|
||||||
|
parent.style.position = 'relative'
|
||||||
|
}
|
||||||
|
|
||||||
indicator = document.createElement('div')
|
indicator = document.createElement('div')
|
||||||
indicator.classList.add('sortable-position-indicator')
|
indicator.classList.add('sortable-position-indicator')
|
||||||
parent.insertBefore(indicator, parent.firstChild)
|
parent.insertBefore(indicator, parent.firstChild)
|
||||||
|
|
|
@ -6,7 +6,7 @@
|
||||||
}}</label>
|
}}</label>
|
||||||
<div class="control__elements">
|
<div class="control__elements">
|
||||||
<a
|
<a
|
||||||
:ref="'color-select'"
|
ref="color-select"
|
||||||
:class="'rating-field__color' + ' background-color--' + values.color"
|
:class="'rating-field__color' + ' background-color--' + values.color"
|
||||||
@click="openColor()"
|
@click="openColor()"
|
||||||
>
|
>
|
||||||
|
|
|
@ -59,8 +59,9 @@
|
||||||
v-if="dec.valueProviderType"
|
v-if="dec.valueProviderType"
|
||||||
:view="view"
|
:view="view"
|
||||||
:table="table"
|
:table="table"
|
||||||
:fields="allFields"
|
:primary="primary"
|
||||||
:read-only="readOnly || dec.decoration._.loading"
|
:fields="fields"
|
||||||
|
:read-only="readOnly"
|
||||||
:options="dec.decoration.value_provider_conf"
|
:options="dec.decoration.value_provider_conf"
|
||||||
@update="updateDecorationOptions(dec.decoration, $event)"
|
@update="updateDecorationOptions(dec.decoration, $event)"
|
||||||
/>
|
/>
|
||||||
|
@ -192,7 +193,7 @@ export default {
|
||||||
value_provider_type: valueProviderType.getType(),
|
value_provider_type: valueProviderType.getType(),
|
||||||
value_provider_conf: valueProviderType.getDefaultConfiguration({
|
value_provider_conf: valueProviderType.getDefaultConfiguration({
|
||||||
view: this.view,
|
view: this.view,
|
||||||
fields: this.fields,
|
fields: this.allFields,
|
||||||
}),
|
}),
|
||||||
},
|
},
|
||||||
decoration,
|
decoration,
|
||||||
|
|
|
@ -0,0 +1,255 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<!--
|
||||||
|
Here we use the index as key to avoid loosing focus when filter id change.
|
||||||
|
-->
|
||||||
|
<div
|
||||||
|
v-for="(filter, index) in filters"
|
||||||
|
:key="index"
|
||||||
|
class="filters__item"
|
||||||
|
:class="{
|
||||||
|
'filters__item--loading': filter._ && filter._.loading,
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<a
|
||||||
|
v-if="!disableFilter"
|
||||||
|
class="filters__remove"
|
||||||
|
@click="deleteFilter(filter)"
|
||||||
|
>
|
||||||
|
<i class="fas fa-times"></i>
|
||||||
|
</a>
|
||||||
|
<div class="filters__operator">
|
||||||
|
<span v-if="index === 0">{{ $t('viewFilterContext.where') }}</span>
|
||||||
|
<Dropdown
|
||||||
|
v-if="index === 1 && !disableFilter"
|
||||||
|
:value="filterType"
|
||||||
|
:show-search="false"
|
||||||
|
class="dropdown--floating dropdown--tiny"
|
||||||
|
@input="selectBooleanOperator($event)"
|
||||||
|
>
|
||||||
|
<DropdownItem
|
||||||
|
:name="$t('viewFilterContext.and')"
|
||||||
|
value="AND"
|
||||||
|
></DropdownItem>
|
||||||
|
<DropdownItem
|
||||||
|
:name="$t('viewFilterContext.or')"
|
||||||
|
value="OR"
|
||||||
|
></DropdownItem>
|
||||||
|
</Dropdown>
|
||||||
|
<span v-if="index > 1 || (index > 0 && disableFilter)">
|
||||||
|
{{
|
||||||
|
filterType === 'AND'
|
||||||
|
? $t('viewFilterContext.and')
|
||||||
|
: $t('viewFilterContext.or')
|
||||||
|
}}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div class="filters__field">
|
||||||
|
<Dropdown
|
||||||
|
:value="filter.field"
|
||||||
|
:disabled="disableFilter"
|
||||||
|
class="dropdown--floating dropdown--tiny"
|
||||||
|
@input="updateFilter(filter, { field: $event })"
|
||||||
|
>
|
||||||
|
<DropdownItem
|
||||||
|
:key="'primary-' + primary.id"
|
||||||
|
:name="primary.name"
|
||||||
|
:value="primary.id"
|
||||||
|
:disabled="hasNoCompatibleFilterTypes(primary, filterTypes)"
|
||||||
|
></DropdownItem>
|
||||||
|
<DropdownItem
|
||||||
|
v-for="field in fields"
|
||||||
|
:key="'field-' + field.id"
|
||||||
|
:name="field.name"
|
||||||
|
:value="field.id"
|
||||||
|
:disabled="hasNoCompatibleFilterTypes(field, filterTypes)"
|
||||||
|
></DropdownItem>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<div class="filters__type">
|
||||||
|
<Dropdown
|
||||||
|
:disabled="disableFilter"
|
||||||
|
:value="filter.type"
|
||||||
|
class="dropdown--floating dropdown--tiny"
|
||||||
|
@input="updateFilter(filter, { type: $event })"
|
||||||
|
>
|
||||||
|
<DropdownItem
|
||||||
|
v-for="fType in allowedFilters(
|
||||||
|
filterTypes,
|
||||||
|
primary,
|
||||||
|
fields,
|
||||||
|
filter.field
|
||||||
|
)"
|
||||||
|
:key="fType.type"
|
||||||
|
:name="fType.getName()"
|
||||||
|
:value="fType.type"
|
||||||
|
></DropdownItem>
|
||||||
|
</Dropdown>
|
||||||
|
</div>
|
||||||
|
<div class="filters__value">
|
||||||
|
<component
|
||||||
|
:is="getInputComponent(filter.type, filter.field)"
|
||||||
|
:ref="`filter-value-${index}`"
|
||||||
|
:filter="filter"
|
||||||
|
:view="view"
|
||||||
|
:fields="fields"
|
||||||
|
:primary="primary"
|
||||||
|
:disabled="disableFilter"
|
||||||
|
:read-only="readOnly"
|
||||||
|
@input="updateFilter(filter, { value: $event })"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
export default {
|
||||||
|
name: 'ViewFieldConditionsForm',
|
||||||
|
props: {
|
||||||
|
filters: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
disableFilter: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
filterType: {
|
||||||
|
type: String,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
primary: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
fields: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
view: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
readOnly: {
|
||||||
|
type: Boolean,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
filterTypes() {
|
||||||
|
return this.$registry.getAll('viewFilter')
|
||||||
|
},
|
||||||
|
localFilters() {
|
||||||
|
// Copy the filters
|
||||||
|
return [...this.filters]
|
||||||
|
},
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
/**
|
||||||
|
* When a filter has been created or removed we want to focus on last value. By
|
||||||
|
* watching localFilters instead of filters, the new and old values are differents.
|
||||||
|
*/
|
||||||
|
localFilters(value, old) {
|
||||||
|
if (value.length !== old.length) {
|
||||||
|
this.$nextTick(() => {
|
||||||
|
this.focusValue(value.length - 1)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
focusValue(position) {
|
||||||
|
const ref = `filter-value-${position}`
|
||||||
|
if (
|
||||||
|
position >= 0 &&
|
||||||
|
Object.prototype.hasOwnProperty.call(this.$refs, ref) &&
|
||||||
|
this.$refs[ref][0] &&
|
||||||
|
Object.prototype.hasOwnProperty.call(this.$refs[ref][0], 'focus')
|
||||||
|
) {
|
||||||
|
this.$refs[ref][0].focus()
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Indicates if the field has any compatible filter types.
|
||||||
|
*/
|
||||||
|
hasNoCompatibleFilterTypes(field, filterTypes) {
|
||||||
|
for (const type in filterTypes) {
|
||||||
|
if (filterTypes[type].fieldIsCompatible(field)) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Returns a list of filter types that are allowed for the given fieldId.
|
||||||
|
*/
|
||||||
|
allowedFilters(filterTypes, primary, fields, fieldId) {
|
||||||
|
const field =
|
||||||
|
primary.id === fieldId ? primary : fields.find((f) => f.id === fieldId)
|
||||||
|
return Object.values(filterTypes).filter((filterType) => {
|
||||||
|
return field !== undefined && filterType.fieldIsCompatible(field)
|
||||||
|
})
|
||||||
|
},
|
||||||
|
deleteFilter(filter) {
|
||||||
|
this.$emit('deleteFilter', filter)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Updates a filter with the given values. Some data manipulation will also be done
|
||||||
|
* because some filter types are not compatible with certain field types.
|
||||||
|
*/
|
||||||
|
updateFilter(filter, values) {
|
||||||
|
const field = Object.prototype.hasOwnProperty.call(values, 'field')
|
||||||
|
? values.field
|
||||||
|
: filter.field
|
||||||
|
const type = Object.prototype.hasOwnProperty.call(values, 'type')
|
||||||
|
? values.type
|
||||||
|
: filter.type
|
||||||
|
const value = Object.prototype.hasOwnProperty.call(values, 'value')
|
||||||
|
? values.value
|
||||||
|
: filter.value
|
||||||
|
|
||||||
|
// If the field has changed we need to check if the filter type is compatible
|
||||||
|
// and if not we are going to choose the first compatible type.
|
||||||
|
if (Object.prototype.hasOwnProperty.call(values, 'field')) {
|
||||||
|
const allowedFilterTypes = this.allowedFilters(
|
||||||
|
this.filterTypes,
|
||||||
|
this.primary,
|
||||||
|
this.fields,
|
||||||
|
field
|
||||||
|
).map((filter) => filter.type)
|
||||||
|
if (!allowedFilterTypes.includes(type)) {
|
||||||
|
values.type = allowedFilterTypes[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the type or value has changed it could be that the value needs to be
|
||||||
|
// formatted or prepared.
|
||||||
|
if (
|
||||||
|
Object.prototype.hasOwnProperty.call(values, 'type') ||
|
||||||
|
Object.prototype.hasOwnProperty.call(values, 'value')
|
||||||
|
) {
|
||||||
|
const filterType = this.$registry.get('viewFilter', type)
|
||||||
|
values.value = filterType.prepareValue(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
this.$emit('updateFilter', { filter, values })
|
||||||
|
},
|
||||||
|
|
||||||
|
selectBooleanOperator(value) {
|
||||||
|
this.$emit('selectOperator', value)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Returns the input component related to the filter type. This component is
|
||||||
|
* responsible for updating the filter value.
|
||||||
|
*/
|
||||||
|
getInputComponent(type, fieldId) {
|
||||||
|
const field =
|
||||||
|
this.primary.id === fieldId
|
||||||
|
? this.primary
|
||||||
|
: this.fields.find(({ id }) => id === fieldId)
|
||||||
|
return this.$registry.get('viewFilter', type).getInputComponent(field)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div>
|
<div>
|
||||||
<div v-show="view.filters.length === 0">
|
<div v-if="view.filters.length === 0">
|
||||||
<div class="filters__none">
|
<div class="filters__none">
|
||||||
<div class="filters__none-title">
|
<div class="filters__none-title">
|
||||||
{{ $t('viewFilterContext.noFilterTitle') }}
|
{{ $t('viewFilterContext.noFilterTitle') }}
|
||||||
|
@ -10,110 +10,18 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<ViewFieldConditionsForm
|
||||||
v-for="(filter, index) in view.filters"
|
:filters="view.filters"
|
||||||
:key="filter.id"
|
:disable-filter="disableFilter"
|
||||||
class="filters__item"
|
:filter-type="view.filter_type"
|
||||||
:class="{
|
:primary="primary"
|
||||||
'filters__item--loading': filter._.loading,
|
:fields="fields"
|
||||||
}"
|
:view="view"
|
||||||
>
|
:read-only="readOnly"
|
||||||
<a
|
@deleteFilter="deleteFilter($event)"
|
||||||
v-if="!disableFilter"
|
@updateFilter="updateFilter($event)"
|
||||||
class="filters__remove"
|
@selectOperator="updateView(view, { filter_type: $event })"
|
||||||
@click.stop.prevent="deleteFilter(filter)"
|
/>
|
||||||
>
|
|
||||||
<i class="fas fa-times"></i>
|
|
||||||
</a>
|
|
||||||
<div class="filters__operator">
|
|
||||||
<span v-if="index === 0">{{ $t('viewFilterContext.where') }}</span>
|
|
||||||
<Dropdown
|
|
||||||
v-if="index === 1 && !disableFilter"
|
|
||||||
:value="view.filter_type"
|
|
||||||
:show-search="false"
|
|
||||||
class="dropdown--floating dropdown--tiny"
|
|
||||||
@input="updateView(view, { filter_type: $event })"
|
|
||||||
>
|
|
||||||
<DropdownItem
|
|
||||||
:name="$t('viewFilterContext.and')"
|
|
||||||
value="AND"
|
|
||||||
></DropdownItem>
|
|
||||||
<DropdownItem
|
|
||||||
:name="$t('viewFilterContext.or')"
|
|
||||||
value="OR"
|
|
||||||
></DropdownItem>
|
|
||||||
</Dropdown>
|
|
||||||
<span
|
|
||||||
v-if="
|
|
||||||
(index > 1 || (index > 0 && disableFilter)) &&
|
|
||||||
view.filter_type === 'AND'
|
|
||||||
"
|
|
||||||
>{{ $t('viewFilterContext.and') }}</span
|
|
||||||
>
|
|
||||||
<span
|
|
||||||
v-if="
|
|
||||||
(index > 1 || (index > 0 && disableFilter)) &&
|
|
||||||
view.filter_type === 'OR'
|
|
||||||
"
|
|
||||||
>{{ $t('viewFilterContext.or') }}</span
|
|
||||||
>
|
|
||||||
</div>
|
|
||||||
<div class="filters__field">
|
|
||||||
<Dropdown
|
|
||||||
:value="filter.field"
|
|
||||||
:disabled="disableFilter"
|
|
||||||
class="dropdown--floating dropdown--tiny"
|
|
||||||
@input="updateFilter(filter, { field: $event })"
|
|
||||||
>
|
|
||||||
<DropdownItem
|
|
||||||
:key="'filter-field-' + filter.id + '-' + primary.id"
|
|
||||||
:name="primary.name"
|
|
||||||
:value="primary.id"
|
|
||||||
:disabled="hasNoCompatibleFilterTypes(primary, filterTypes)"
|
|
||||||
></DropdownItem>
|
|
||||||
<DropdownItem
|
|
||||||
v-for="field in fields"
|
|
||||||
:key="'filter-field-' + filter.id + '-' + field.id"
|
|
||||||
:name="field.name"
|
|
||||||
:value="field.id"
|
|
||||||
:disabled="hasNoCompatibleFilterTypes(field, filterTypes)"
|
|
||||||
></DropdownItem>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
<div class="filters__type">
|
|
||||||
<Dropdown
|
|
||||||
:disabled="disableFilter"
|
|
||||||
:value="filter.type"
|
|
||||||
class="dropdown--floating dropdown--tiny"
|
|
||||||
@input="updateFilter(filter, { type: $event })"
|
|
||||||
>
|
|
||||||
<DropdownItem
|
|
||||||
v-for="filterType in allowedFilters(
|
|
||||||
filterTypes,
|
|
||||||
primary,
|
|
||||||
fields,
|
|
||||||
filter.field
|
|
||||||
)"
|
|
||||||
:key="filterType.type"
|
|
||||||
:name="filterType.getName()"
|
|
||||||
:value="filterType.type"
|
|
||||||
></DropdownItem>
|
|
||||||
</Dropdown>
|
|
||||||
</div>
|
|
||||||
<div class="filters__value">
|
|
||||||
<component
|
|
||||||
:is="getInputComponent(filter.type, filter.field)"
|
|
||||||
:ref="'filter-' + filter.id + '-value'"
|
|
||||||
:filter="filter"
|
|
||||||
:view="view"
|
|
||||||
:fields="fields"
|
|
||||||
:primary="primary"
|
|
||||||
:disabled="disableFilter"
|
|
||||||
:read-only="readOnly"
|
|
||||||
@input="updateFilter(filter, { value: $event })"
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div v-if="!disableFilter" class="filters_footer">
|
<div v-if="!disableFilter" class="filters_footer">
|
||||||
<a class="filters__add" @click.prevent="addFilter()">
|
<a class="filters__add" @click.prevent="addFilter()">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
|
@ -132,9 +40,13 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||||
|
import ViewFieldConditionsForm from '@baserow/modules/database/components/view/ViewFieldConditionsForm'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ViewFilterForm',
|
name: 'ViewFilterForm',
|
||||||
|
components: {
|
||||||
|
ViewFieldConditionsForm,
|
||||||
|
},
|
||||||
props: {
|
props: {
|
||||||
primary: {
|
primary: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -162,54 +74,10 @@ export default {
|
||||||
return this.$registry.getAll('viewFilter')
|
return this.$registry.getAll('viewFilter')
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
beforeMount() {
|
|
||||||
this.$bus.$on('view-filter-created', this.filterCreated)
|
|
||||||
},
|
|
||||||
beforeDestroy() {
|
|
||||||
this.$bus.$off('view-filter-created', this.filterCreated)
|
|
||||||
},
|
|
||||||
methods: {
|
methods: {
|
||||||
/**
|
async addFilter(values) {
|
||||||
* When the filter has been created we want to focus on the value.
|
|
||||||
*/
|
|
||||||
filterCreated({ filter }) {
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.focusValue(filter)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
focusValue(filter) {
|
|
||||||
const ref = 'filter-' + filter.id + '-value'
|
|
||||||
if (
|
|
||||||
Object.prototype.hasOwnProperty.call(this.$refs, ref) &&
|
|
||||||
Object.prototype.hasOwnProperty.call(this.$refs[ref][0], 'focus')
|
|
||||||
) {
|
|
||||||
this.$refs[ref][0].focus()
|
|
||||||
}
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Indicates if the field has any compatible filter types.
|
|
||||||
*/
|
|
||||||
hasNoCompatibleFilterTypes(field, filterTypes) {
|
|
||||||
for (const type in filterTypes) {
|
|
||||||
if (filterTypes[type].fieldIsCompatible(field)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
},
|
|
||||||
/**
|
|
||||||
* Returns a list of filter types that are allowed for the given fieldId.
|
|
||||||
*/
|
|
||||||
allowedFilters(filterTypes, primary, fields, fieldId) {
|
|
||||||
const field =
|
|
||||||
primary.id === fieldId ? primary : fields.find((f) => f.id === fieldId)
|
|
||||||
return Object.values(filterTypes).filter((filterType) => {
|
|
||||||
return field !== undefined && filterType.fieldIsCompatible(field)
|
|
||||||
})
|
|
||||||
},
|
|
||||||
async addFilter() {
|
|
||||||
try {
|
try {
|
||||||
const { filter } = await this.$store.dispatch('view/createFilter', {
|
await this.$store.dispatch('view/createFilter', {
|
||||||
view: this.view,
|
view: this.view,
|
||||||
field: this.primary,
|
field: this.primary,
|
||||||
values: {
|
values: {
|
||||||
|
@ -219,11 +87,6 @@ export default {
|
||||||
readOnly: this.readOnly,
|
readOnly: this.readOnly,
|
||||||
})
|
})
|
||||||
this.$emit('changed')
|
this.$emit('changed')
|
||||||
|
|
||||||
// Wait for the filter to be rendered and then focus on the value input.
|
|
||||||
this.$nextTick(() => {
|
|
||||||
this.focusValue(filter)
|
|
||||||
})
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
notifyIf(error, 'view')
|
notifyIf(error, 'view')
|
||||||
}
|
}
|
||||||
|
@ -244,41 +107,7 @@ export default {
|
||||||
* Updates a filter with the given values. Some data manipulation will also be done
|
* Updates a filter with the given values. Some data manipulation will also be done
|
||||||
* because some filter types are not compatible with certain field types.
|
* because some filter types are not compatible with certain field types.
|
||||||
*/
|
*/
|
||||||
async updateFilter(filter, values) {
|
async updateFilter({ filter, values }) {
|
||||||
const field = Object.prototype.hasOwnProperty.call(values, 'field')
|
|
||||||
? values.field
|
|
||||||
: filter.field
|
|
||||||
const type = Object.prototype.hasOwnProperty.call(values, 'type')
|
|
||||||
? values.type
|
|
||||||
: filter.type
|
|
||||||
const value = Object.prototype.hasOwnProperty.call(values, 'value')
|
|
||||||
? values.value
|
|
||||||
: filter.value
|
|
||||||
|
|
||||||
// If the field has changed we need to check if the filter type is compatible
|
|
||||||
// and if not we are going to choose the first compatible type.
|
|
||||||
if (Object.prototype.hasOwnProperty.call(values, 'field')) {
|
|
||||||
const allowedFilterTypes = this.allowedFilters(
|
|
||||||
this.filterTypes,
|
|
||||||
this.primary,
|
|
||||||
this.fields,
|
|
||||||
field
|
|
||||||
).map((filter) => filter.type)
|
|
||||||
if (!allowedFilterTypes.includes(type)) {
|
|
||||||
values.type = allowedFilterTypes[0]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// If the type or value has changed it could be that the value needs to be
|
|
||||||
// formatted or prepared.
|
|
||||||
if (
|
|
||||||
Object.prototype.hasOwnProperty.call(values, 'type') ||
|
|
||||||
Object.prototype.hasOwnProperty.call(values, 'value')
|
|
||||||
) {
|
|
||||||
const filterType = this.$registry.get('viewFilter', type)
|
|
||||||
values.value = filterType.prepareValue(value)
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.$store.dispatch('view/updateFilter', {
|
await this.$store.dispatch('view/updateFilter', {
|
||||||
filter,
|
filter,
|
||||||
|
@ -310,14 +139,6 @@ export default {
|
||||||
|
|
||||||
this.$store.dispatch('view/setItemLoading', { view, value: false })
|
this.$store.dispatch('view/setItemLoading', { view, value: false })
|
||||||
},
|
},
|
||||||
/**
|
|
||||||
* Returns the input component related to the filter type. This component is
|
|
||||||
* responsible for updating the filter value.
|
|
||||||
*/
|
|
||||||
getInputComponent(type, fieldId) {
|
|
||||||
const field = this.fields.find(({ id }) => id === fieldId)
|
|
||||||
return this.$registry.get('viewFilter', type).getInputComponent(field)
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -11,15 +11,22 @@
|
||||||
<a
|
<a
|
||||||
v-else
|
v-else
|
||||||
class="filters__value-link-row"
|
class="filters__value-link-row"
|
||||||
:class="{ 'filters__value-link-row--disabled': disabled }"
|
:class="{
|
||||||
|
'filters__value-link-row--disabled': disabled,
|
||||||
|
'filters__value-link-row--loading': loading,
|
||||||
|
}"
|
||||||
@click.prevent="!disabled && $refs.selectModal.show()"
|
@click.prevent="!disabled && $refs.selectModal.show()"
|
||||||
>
|
>
|
||||||
<template v-if="valid">
|
<template v-if="!loading">
|
||||||
{{ name || $t('viewFilterTypeLinkRow.unnamed', { value: filter.value }) }}
|
<template v-if="valid">
|
||||||
|
{{
|
||||||
|
name || $t('viewFilterTypeLinkRow.unnamed', { value: filter.value })
|
||||||
|
}}
|
||||||
|
</template>
|
||||||
|
<div v-else class="filters__value-link-row-choose">
|
||||||
|
{{ $t('viewFilterTypeLinkRow.choose') }}
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<div v-else class="filters__value-link-row-choose">
|
|
||||||
{{ $t('viewFilterTypeLinkRow.choose') }}
|
|
||||||
</div>
|
|
||||||
<SelectRowModal
|
<SelectRowModal
|
||||||
v-if="!disabled"
|
v-if="!disabled"
|
||||||
ref="selectModal"
|
ref="selectModal"
|
||||||
|
@ -35,6 +42,7 @@ import PaginatedDropdown from '@baserow/modules/core/components/PaginatedDropdow
|
||||||
import SelectRowModal from '@baserow/modules/database/components/row/SelectRowModal'
|
import SelectRowModal from '@baserow/modules/database/components/row/SelectRowModal'
|
||||||
import viewFilter from '@baserow/modules/database/mixins/viewFilter'
|
import viewFilter from '@baserow/modules/database/mixins/viewFilter'
|
||||||
import ViewService from '@baserow/modules/database/services/view'
|
import ViewService from '@baserow/modules/database/services/view'
|
||||||
|
import RowService from '@baserow/modules/database/services/row'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ViewFilterTypeLinkRow',
|
name: 'ViewFilterTypeLinkRow',
|
||||||
|
@ -43,41 +51,55 @@ export default {
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
name: '',
|
name: '',
|
||||||
|
rowInfo: null,
|
||||||
|
loading: false,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
valid() {
|
valid() {
|
||||||
return this.isValidValue(this.filter.value)
|
return isNumeric(this.filter.value)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'filter.preload_values'(value) {
|
'filter.value'() {
|
||||||
this.setNameFromPreloadValues(value)
|
this.setName()
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
mounted() {
|
mounted() {
|
||||||
this.setNameFromPreloadValues(this.filter.preload_values)
|
this.setName()
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
setNameFromRow(row, primary) {
|
async setName() {
|
||||||
this.name = this.$registry
|
const { value, preload_values: { display_name: displayName } = {} } =
|
||||||
.get('field', primary.type)
|
this.filter
|
||||||
.toHumanReadableString(primary, row[`field_${primary.id}`])
|
|
||||||
},
|
|
||||||
setNameFromPreloadValues(values) {
|
|
||||||
if (Object.prototype.hasOwnProperty.call(values, 'display_name')) {
|
|
||||||
this.name = values.display_name
|
|
||||||
}
|
|
||||||
},
|
|
||||||
isValidValue() {
|
|
||||||
if (!isNumeric(this.filter.value)) {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
return true
|
if (!value) {
|
||||||
|
this.name = ''
|
||||||
|
} else if (displayName) {
|
||||||
|
// set the name from preload_values
|
||||||
|
this.name = displayName
|
||||||
|
} else if (this.rowInfo) {
|
||||||
|
// Set the name from previous row info
|
||||||
|
const { row, primary } = this.rowInfo
|
||||||
|
this.name = this.$registry
|
||||||
|
.get('field', primary.type)
|
||||||
|
.toHumanReadableString(primary, row[`field_${primary.id}`])
|
||||||
|
this.rowInfo = null
|
||||||
|
} else {
|
||||||
|
// Get the name from server
|
||||||
|
this.loading = true
|
||||||
|
try {
|
||||||
|
this.name = await RowService(this.$client).getName(
|
||||||
|
this.field.link_row_table,
|
||||||
|
value
|
||||||
|
)
|
||||||
|
} finally {
|
||||||
|
this.loading = false
|
||||||
|
}
|
||||||
|
}
|
||||||
},
|
},
|
||||||
setValue({ row, primary }) {
|
setValue({ row, primary }) {
|
||||||
this.setNameFromRow(row, primary)
|
this.rowInfo = { row, primary }
|
||||||
this.$emit('input', row.id.toString())
|
this.$emit('input', row.id.toString())
|
||||||
},
|
},
|
||||||
fetchPage(page, search) {
|
fetchPage(page, search) {
|
||||||
|
|
|
@ -16,6 +16,7 @@
|
||||||
ref="left"
|
ref="left"
|
||||||
class="grid-view__left"
|
class="grid-view__left"
|
||||||
:fields="leftFields"
|
:fields="leftFields"
|
||||||
|
:all-table-fields="allTableFields"
|
||||||
:table="table"
|
:table="table"
|
||||||
:view="view"
|
:view="view"
|
||||||
:include-field-width-handles="false"
|
:include-field-width-handles="false"
|
||||||
|
@ -64,6 +65,7 @@
|
||||||
ref="right"
|
ref="right"
|
||||||
class="grid-view__right"
|
class="grid-view__right"
|
||||||
:fields="visibleFields"
|
:fields="visibleFields"
|
||||||
|
:all-table-fields="allTableFields"
|
||||||
:table="table"
|
:table="table"
|
||||||
:view="view"
|
:view="view"
|
||||||
:include-add-field="true"
|
:include-add-field="true"
|
||||||
|
@ -276,6 +278,9 @@ export default {
|
||||||
leftWidth() {
|
leftWidth() {
|
||||||
return this.leftFieldsWidth + this.gridViewRowDetailsWidth
|
return this.leftFieldsWidth + this.gridViewRowDetailsWidth
|
||||||
},
|
},
|
||||||
|
allTableFields() {
|
||||||
|
return [this.primary, ...this.fields]
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
fieldOptions: {
|
fieldOptions: {
|
||||||
|
|
|
@ -70,7 +70,7 @@
|
||||||
:is="dec.component"
|
:is="dec.component"
|
||||||
v-for="dec in firstCellDecorations"
|
v-for="dec in firstCellDecorations"
|
||||||
:key="dec.decoration.id"
|
:key="dec.decoration.id"
|
||||||
:value="dec.propsFn(row).value"
|
v-bind="dec.propsFn(row)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -41,6 +41,10 @@ export default {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
allTableFields: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
leftOffset: {
|
leftOffset: {
|
||||||
type: Number,
|
type: Number,
|
||||||
required: false,
|
required: false,
|
||||||
|
@ -86,11 +90,12 @@ export default {
|
||||||
'decoratorValueProvider',
|
'decoratorValueProvider',
|
||||||
decoration.value_provider_type
|
decoration.value_provider_type
|
||||||
)
|
)
|
||||||
|
|
||||||
deco.propsFn = (row) => {
|
deco.propsFn = (row) => {
|
||||||
return {
|
return {
|
||||||
value: deco.valueProviderType.getValue({
|
value: deco.valueProviderType.getValue({
|
||||||
row,
|
row,
|
||||||
fields: this.allFields,
|
fields: this.allTableFields,
|
||||||
options: decoration.value_provider_conf,
|
options: decoration.value_provider_conf,
|
||||||
}),
|
}),
|
||||||
}
|
}
|
||||||
|
|
|
@ -52,6 +52,7 @@
|
||||||
:view="view"
|
:view="view"
|
||||||
:fields="fieldsToRender"
|
:fields="fieldsToRender"
|
||||||
:all-fields="fields"
|
:all-fields="fields"
|
||||||
|
:all-table-fields="allTableFields"
|
||||||
:left-offset="fieldsLeftOffset"
|
:left-offset="fieldsLeftOffset"
|
||||||
:include-row-details="includeRowDetails"
|
:include-row-details="includeRowDetails"
|
||||||
:read-only="readOnly"
|
:read-only="readOnly"
|
||||||
|
@ -126,6 +127,10 @@ export default {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
allTableFields: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
table: {
|
table: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
|
|
|
@ -1,4 +1,28 @@
|
||||||
|
let pendingGetQueries = {}
|
||||||
|
let delay = null
|
||||||
|
const GRACE_DELAY = 50 // ms before querying the backend with a get query
|
||||||
|
|
||||||
export default (client) => {
|
export default (client) => {
|
||||||
|
const getNameCallback = async () => {
|
||||||
|
const config = {}
|
||||||
|
config.params = Object.fromEntries(
|
||||||
|
Object.entries(pendingGetQueries).map(([tableId, rows]) => {
|
||||||
|
const rowIds = Object.keys(rows)
|
||||||
|
return [`table__${tableId}`, rowIds.join(',')]
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
const { data } = await client.get(`/database/rows/names/`, config)
|
||||||
|
|
||||||
|
Object.entries(data).forEach(([tableId, rows]) => {
|
||||||
|
Object.entries(rows).forEach(([rowId, rowName]) => {
|
||||||
|
pendingGetQueries[tableId][rowId].forEach((resolve) => resolve(rowName))
|
||||||
|
})
|
||||||
|
})
|
||||||
|
pendingGetQueries = {}
|
||||||
|
delay = null
|
||||||
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
get(tableId, rowId) {
|
get(tableId, rowId) {
|
||||||
return client.get(`/database/rows/table/${tableId}/${rowId}/`)
|
return client.get(`/database/rows/table/${tableId}/${rowId}/`)
|
||||||
|
@ -17,6 +41,25 @@ export default (client) => {
|
||||||
|
|
||||||
return client.get(`/database/rows/table/${tableId}/`, config)
|
return client.get(`/database/rows/table/${tableId}/`, config)
|
||||||
},
|
},
|
||||||
|
/**
|
||||||
|
* Returns the name of specified table row. Batch consecutive queries into one
|
||||||
|
* during the defined GRACE_TIME.
|
||||||
|
*/
|
||||||
|
getName(tableId, rowId) {
|
||||||
|
return new Promise((resolve) => {
|
||||||
|
clearTimeout(delay)
|
||||||
|
|
||||||
|
if (!pendingGetQueries[tableId]) {
|
||||||
|
pendingGetQueries[tableId] = {}
|
||||||
|
}
|
||||||
|
if (!pendingGetQueries[tableId][rowId]) {
|
||||||
|
pendingGetQueries[tableId][rowId] = []
|
||||||
|
}
|
||||||
|
pendingGetQueries[tableId][rowId].push(resolve)
|
||||||
|
|
||||||
|
delay = setTimeout(getNameCallback, GRACE_DELAY)
|
||||||
|
})
|
||||||
|
},
|
||||||
create(tableId, values, beforeId = null) {
|
create(tableId, values, beforeId = null) {
|
||||||
const config = { params: {} }
|
const config = { params: {} }
|
||||||
|
|
||||||
|
|
|
@ -479,6 +479,10 @@ export const actions = {
|
||||||
|
|
||||||
commit('ADD_FILTER', { view, filter })
|
commit('ADD_FILTER', { view, filter })
|
||||||
|
|
||||||
|
if (emitEvent) {
|
||||||
|
this.$bus.$emit('view-filter-created', { view, filter })
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (!readOnly) {
|
if (!readOnly) {
|
||||||
const { data } = await FilterService(this.$client).create(
|
const { data } = await FilterService(this.$client).create(
|
||||||
|
@ -487,10 +491,6 @@ export const actions = {
|
||||||
)
|
)
|
||||||
commit('FINALIZE_FILTER', { view, oldId: filter.id, id: data.id })
|
commit('FINALIZE_FILTER', { view, oldId: filter.id, id: data.id })
|
||||||
}
|
}
|
||||||
|
|
||||||
if (emitEvent) {
|
|
||||||
this.$bus.$emit('view-filter-created', { view, filter })
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
commit('DELETE_FILTER', { view, id: filter.id })
|
commit('DELETE_FILTER', { view, id: filter.id })
|
||||||
throw error
|
throw error
|
||||||
|
|
|
@ -14,6 +14,7 @@
|
||||||
"dev": "nuxt --hostname 0.0.0.0",
|
"dev": "nuxt --hostname 0.0.0.0",
|
||||||
"start": "nuxt start --hostname 0.0.0.0",
|
"start": "nuxt start --hostname 0.0.0.0",
|
||||||
"eslint": "eslint -c .eslintrc.js --ext .js,.vue . ../premium/web-frontend",
|
"eslint": "eslint -c .eslintrc.js --ext .js,.vue . ../premium/web-frontend",
|
||||||
|
"lint": "yarn eslint && yarn stylelint",
|
||||||
"stylelint": "stylelint **/*.scss ../premium/web-frontend/**/*.scss --syntax scss",
|
"stylelint": "stylelint **/*.scss ../premium/web-frontend/**/*.scss --syntax scss",
|
||||||
"jest": "jest --verbose false",
|
"jest": "jest --verbose false",
|
||||||
"test": "yarn jest"
|
"test": "yarn jest"
|
||||||
|
@ -79,4 +80,4 @@
|
||||||
"stylelint-webpack-plugin": "^3.0.1",
|
"stylelint-webpack-plugin": "^3.0.1",
|
||||||
"vue-jest": "^3.0.3"
|
"vue-jest": "^3.0.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -207,13 +207,12 @@ exports[`GridViewRows component with decoration Should show can add decorator to
|
||||||
/>
|
/>
|
||||||
|
|
||||||
Fake value provider
|
Fake value provider
|
||||||
|
|
||||||
|
<i
|
||||||
|
class="dropdown__toggle-icon fas fa-caret-down"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<i
|
|
||||||
class="dropdown__toggle-icon fas fa-caret-down"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -510,13 +509,12 @@ exports[`GridViewRows component with decoration Should show unavailable decorato
|
||||||
/>
|
/>
|
||||||
|
|
||||||
Fake value provider
|
Fake value provider
|
||||||
|
|
||||||
|
<i
|
||||||
|
class="dropdown__toggle-icon fas fa-caret-down"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<i
|
|
||||||
class="dropdown__toggle-icon fas fa-caret-down"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@ -762,13 +760,12 @@ exports[`GridViewRows component with decoration View with decoration configured
|
||||||
/>
|
/>
|
||||||
|
|
||||||
Fake value provider
|
Fake value provider
|
||||||
|
|
||||||
|
<i
|
||||||
|
class="dropdown__toggle-icon fas fa-caret-down"
|
||||||
|
/>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<i
|
|
||||||
class="dropdown__toggle-icon fas fa-caret-down"
|
|
||||||
/>
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
File diff suppressed because it is too large
Load diff
|
@ -93,7 +93,7 @@ describe('ViewFilterForm component', () => {
|
||||||
props = {
|
props = {
|
||||||
primary: {},
|
primary: {},
|
||||||
fields: [],
|
fields: [],
|
||||||
view: { filters: {}, _: {} },
|
view: { filters: [], _: {} },
|
||||||
readOnly: false,
|
readOnly: false,
|
||||||
},
|
},
|
||||||
listeners = {}
|
listeners = {}
|
||||||
|
|
Loading…
Add table
Reference in a new issue