mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-03-28 10:05:10 +00:00
🌈 2️⃣ - Row coloring v2 - Add backend storage
This commit is contained in:
parent
1adf5d9d8e
commit
bc8956b7cc
37 changed files with 1536 additions and 89 deletions
backend
src/baserow
config/settings
contrib/database
api/views
migrations
views
ws/views
test_utils/fixtures
tests/baserow/contrib/database
docs/apis
premium/web-frontend/modules/baserow_premium
web-frontend
modules
core/assets/scss/components/decorator
database
test
fixtures
unit/database
components/view
store/view
|
@ -275,6 +275,7 @@ SPECTACULAR_SETTINGS = {
|
|||
{"name": "Database table views"},
|
||||
{"name": "Database table view filters"},
|
||||
{"name": "Database table view sortings"},
|
||||
{"name": "Database table view decorations"},
|
||||
{"name": "Database table grid view"},
|
||||
{"name": "Database table gallery view"},
|
||||
{"name": "Database table form view"},
|
||||
|
|
|
@ -66,6 +66,16 @@ ERROR_AGGREGATION_TYPE_DOES_NOT_EXIST = (
|
|||
HTTP_400_BAD_REQUEST,
|
||||
"The specified aggregation type does not exist.",
|
||||
)
|
||||
ERROR_VIEW_DECORATION_DOES_NOT_EXIST = (
|
||||
"ERROR_VIEW_DECORATION_DOES_NOT_EXIST",
|
||||
HTTP_404_NOT_FOUND,
|
||||
"The view decoration does not exist.",
|
||||
)
|
||||
ERROR_VIEW_DECORATION_NOT_SUPPORTED = (
|
||||
"ERROR_VIEW_DECORATION_NOT_SUPPORTED",
|
||||
HTTP_400_BAD_REQUEST,
|
||||
"Decoration is not supported for the view type.",
|
||||
)
|
||||
ERROR_CANNOT_SHARE_VIEW_TYPE = (
|
||||
"ERROR_CANNOT_SHARE_VIEW_TYPE",
|
||||
HTTP_400_BAD_REQUEST,
|
||||
|
|
|
@ -10,7 +10,12 @@ from baserow.contrib.database.views.registries import (
|
|||
view_type_registry,
|
||||
view_filter_type_registry,
|
||||
)
|
||||
from baserow.contrib.database.views.models import View, ViewFilter, ViewSort
|
||||
from baserow.contrib.database.views.models import (
|
||||
View,
|
||||
ViewFilter,
|
||||
ViewSort,
|
||||
ViewDecoration,
|
||||
)
|
||||
|
||||
|
||||
class FieldOptionsField(serializers.Field):
|
||||
|
@ -156,11 +161,50 @@ class UpdateViewSortSerializer(serializers.ModelSerializer):
|
|||
extra_kwargs = {"field": {"required": False}, "order": {"required": False}}
|
||||
|
||||
|
||||
class ViewDecorationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ViewDecoration
|
||||
fields = (
|
||||
"id",
|
||||
"view",
|
||||
"type",
|
||||
"value_provider_type",
|
||||
"value_provider_conf",
|
||||
"order",
|
||||
)
|
||||
extra_kwargs = {"id": {"read_only": True}}
|
||||
|
||||
|
||||
class CreateViewDecorationSerializer(serializers.ModelSerializer):
|
||||
class Meta:
|
||||
model = ViewDecoration
|
||||
fields = ("type", "value_provider_type", "value_provider_conf", "order")
|
||||
extra_kwargs = {
|
||||
"value_provider_type": {"required": False, "default": ""},
|
||||
"value_provider_conf": {"required": False, "default": dict},
|
||||
}
|
||||
|
||||
|
||||
class UpdateViewDecorationSerializer(serializers.ModelSerializer):
|
||||
class Meta(CreateViewDecorationSerializer.Meta):
|
||||
model = ViewDecoration
|
||||
fields = ("type", "value_provider_type", "value_provider_conf", "order")
|
||||
extra_kwargs = {
|
||||
"type": {"required": False},
|
||||
"value_provider_type": {"required": False},
|
||||
"value_provider_conf": {"required": False},
|
||||
"order": {"required": False},
|
||||
}
|
||||
|
||||
|
||||
class ViewSerializer(serializers.ModelSerializer):
|
||||
type = serializers.SerializerMethodField()
|
||||
table = TableSerializer()
|
||||
filters = ViewFilterSerializer(many=True, source="viewfilter_set", required=False)
|
||||
sortings = ViewSortSerializer(many=True, source="viewsort_set", required=False)
|
||||
decorations = ViewDecorationSerializer(
|
||||
many=True, source="viewdecoration_set", required=False
|
||||
)
|
||||
|
||||
class Meta:
|
||||
model = View
|
||||
|
@ -174,6 +218,7 @@ class ViewSerializer(serializers.ModelSerializer):
|
|||
"filter_type",
|
||||
"filters",
|
||||
"sortings",
|
||||
"decorations",
|
||||
"filters_disabled",
|
||||
)
|
||||
extra_kwargs = {
|
||||
|
@ -185,19 +230,24 @@ class ViewSerializer(serializers.ModelSerializer):
|
|||
context = kwargs.setdefault("context", {})
|
||||
context["include_filters"] = kwargs.pop("filters", False)
|
||||
context["include_sortings"] = kwargs.pop("sortings", False)
|
||||
context["include_decorations"] = kwargs.pop("decorations", False)
|
||||
super().__init__(*args, **kwargs)
|
||||
|
||||
def to_representation(self, instance):
|
||||
# We remove the fields in to_representation rather than __init__ as otherwise
|
||||
# drf-spectacular will not know that filters and sortings exist as optional
|
||||
# return fields. This way the fields are still dynamic and also show up in the
|
||||
# OpenAPI specification.
|
||||
# drf-spectacular will not know that filters, sortings and decorations exist as
|
||||
# optional return fields.
|
||||
# This way the fields are still dynamic and also show up in the OpenAPI
|
||||
# specification.
|
||||
if not self.context["include_filters"]:
|
||||
self.fields.pop("filters", None)
|
||||
|
||||
if not self.context["include_sortings"]:
|
||||
self.fields.pop("sortings", None)
|
||||
|
||||
if not self.context["include_decorations"]:
|
||||
self.fields.pop("decorations", None)
|
||||
|
||||
return super().to_representation(instance)
|
||||
|
||||
@extend_schema_field(OpenApiTypes.STR)
|
||||
|
|
|
@ -10,6 +10,8 @@ from .views import (
|
|||
ViewFilterView,
|
||||
ViewSortingsView,
|
||||
ViewSortView,
|
||||
ViewDecorationsView,
|
||||
ViewDecorationView,
|
||||
ViewFieldOptionsView,
|
||||
RotateViewSlugView,
|
||||
PublicViewLinkRowFieldLookupView,
|
||||
|
@ -36,6 +38,11 @@ urlpatterns = view_type_registry.api_urls + [
|
|||
re_path(
|
||||
r"sort/(?P<view_sort_id>[0-9]+)/$", ViewSortView.as_view(), name="sort_item"
|
||||
),
|
||||
re_path(
|
||||
r"decoration/(?P<view_decoration_id>[0-9]+)/$",
|
||||
ViewDecorationView.as_view(),
|
||||
name="decoration_item",
|
||||
),
|
||||
re_path(r"(?P<view_id>[0-9]+)/$", ViewView.as_view(), name="item"),
|
||||
re_path(
|
||||
r"(?P<view_id>[0-9]+)/filters/$", ViewFiltersView.as_view(), name="list_filters"
|
||||
|
@ -45,6 +52,11 @@ urlpatterns = view_type_registry.api_urls + [
|
|||
ViewSortingsView.as_view(),
|
||||
name="list_sortings",
|
||||
),
|
||||
re_path(
|
||||
r"(?P<view_id>[0-9]+)/decorations/$",
|
||||
ViewDecorationsView.as_view(),
|
||||
name="list_decorations",
|
||||
),
|
||||
re_path(
|
||||
r"(?P<view_id>[0-9]+)/field-options/$",
|
||||
ViewFieldOptionsView.as_view(),
|
||||
|
|
|
@ -55,7 +55,12 @@ from baserow.contrib.database.fields.exceptions import (
|
|||
from baserow.contrib.database.table.handler import TableHandler
|
||||
from baserow.contrib.database.table.exceptions import TableDoesNotExist
|
||||
from baserow.contrib.database.views.registries import view_type_registry
|
||||
from baserow.contrib.database.views.models import View, ViewFilter, ViewSort
|
||||
from baserow.contrib.database.views.models import (
|
||||
View,
|
||||
ViewFilter,
|
||||
ViewSort,
|
||||
ViewDecoration,
|
||||
)
|
||||
from baserow.contrib.database.views.handler import ViewHandler
|
||||
from baserow.contrib.database.views.exceptions import (
|
||||
ViewDoesNotExist,
|
||||
|
@ -70,6 +75,8 @@ from baserow.contrib.database.views.exceptions import (
|
|||
UnrelatedFieldError,
|
||||
ViewDoesNotSupportFieldOptions,
|
||||
CannotShareViewTypeError,
|
||||
ViewDecorationDoesNotExist,
|
||||
ViewDecorationNotSupported,
|
||||
)
|
||||
|
||||
from .serializers import (
|
||||
|
@ -83,6 +90,9 @@ from .serializers import (
|
|||
ViewSortSerializer,
|
||||
CreateViewSortSerializer,
|
||||
UpdateViewSortSerializer,
|
||||
ViewDecorationSerializer,
|
||||
CreateViewDecorationSerializer,
|
||||
UpdateViewDecorationSerializer,
|
||||
)
|
||||
from .errors import (
|
||||
ERROR_VIEW_DOES_NOT_EXIST,
|
||||
|
@ -94,6 +104,8 @@ from .errors import (
|
|||
ERROR_VIEW_SORT_NOT_SUPPORTED,
|
||||
ERROR_VIEW_SORT_FIELD_ALREADY_EXISTS,
|
||||
ERROR_VIEW_SORT_FIELD_NOT_SUPPORTED,
|
||||
ERROR_VIEW_DECORATION_DOES_NOT_EXIST,
|
||||
ERROR_VIEW_DECORATION_NOT_SUPPORTED,
|
||||
ERROR_UNRELATED_FIELD,
|
||||
ERROR_VIEW_DOES_NOT_SUPPORT_FIELD_OPTIONS,
|
||||
ERROR_CANNOT_SHARE_VIEW_TYPE,
|
||||
|
@ -130,8 +142,9 @@ class ViewsView(APIView):
|
|||
type=OpenApiTypes.STR,
|
||||
description=(
|
||||
"A comma separated list of extra attributes to include on each "
|
||||
"view in the response. The supported attributes are `filters` and "
|
||||
"`sortings`. For example `include=filters,sortings` will add the "
|
||||
"view in the response. The supported attributes are `filters`, "
|
||||
"`sortings` and `decorations`. "
|
||||
"For example `include=filters,sortings` will add the "
|
||||
"attributes `filters` and `sortings` to every returned view, "
|
||||
"containing a list of the views filters and sortings respectively."
|
||||
),
|
||||
|
@ -163,8 +176,8 @@ class ViewsView(APIView):
|
|||
UserNotInGroup: ERROR_USER_NOT_IN_GROUP,
|
||||
}
|
||||
)
|
||||
@allowed_includes("filters", "sortings")
|
||||
def get(self, request, table_id, filters, sortings):
|
||||
@allowed_includes("filters", "sortings", "decorations")
|
||||
def get(self, request, table_id, filters, sortings, decorations):
|
||||
"""
|
||||
Responds with a list of serialized views that belong to the table if the user
|
||||
has access to that group.
|
||||
|
@ -182,9 +195,16 @@ class ViewsView(APIView):
|
|||
if sortings:
|
||||
views = views.prefetch_related("viewsort_set")
|
||||
|
||||
if decorations:
|
||||
views = views.prefetch_related("viewdecoration_set")
|
||||
|
||||
data = [
|
||||
view_type_registry.get_serializer(
|
||||
view, ViewSerializer, filters=filters, sortings=sortings
|
||||
view,
|
||||
ViewSerializer,
|
||||
filters=filters,
|
||||
sortings=sortings,
|
||||
decorations=decorations,
|
||||
).data
|
||||
for view in views
|
||||
]
|
||||
|
@ -205,8 +225,8 @@ class ViewsView(APIView):
|
|||
type=OpenApiTypes.STR,
|
||||
description=(
|
||||
"A comma separated list of extra attributes to include on each "
|
||||
"view in the response. The supported attributes are `filters` and "
|
||||
"`sortings`. "
|
||||
"view in the response. The supported attributes are `filters`, "
|
||||
"`sortings` and `decorations`. "
|
||||
"For example `include=filters,sortings` will add the attributes "
|
||||
"`filters` and `sortings` to every returned view, containing "
|
||||
"a list of the views filters and sortings respectively."
|
||||
|
@ -248,8 +268,8 @@ class ViewsView(APIView):
|
|||
UserNotInGroup: ERROR_USER_NOT_IN_GROUP,
|
||||
}
|
||||
)
|
||||
@allowed_includes("filters", "sortings")
|
||||
def post(self, request, data, table_id, filters, sortings):
|
||||
@allowed_includes("filters", "sortings", "decorations")
|
||||
def post(self, request, data, table_id, filters, sortings, decorations):
|
||||
"""Creates a new view for a user."""
|
||||
|
||||
type_name = data.pop("type")
|
||||
|
@ -260,7 +280,11 @@ class ViewsView(APIView):
|
|||
view = ViewHandler().create_view(request.user, table, type_name, **data)
|
||||
|
||||
serializer = view_type_registry.get_serializer(
|
||||
view, ViewSerializer, filters=filters, sortings=sortings
|
||||
view,
|
||||
ViewSerializer,
|
||||
filters=filters,
|
||||
sortings=sortings,
|
||||
decorations=decorations,
|
||||
)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
@ -282,8 +306,8 @@ class ViewView(APIView):
|
|||
type=OpenApiTypes.STR,
|
||||
description=(
|
||||
"A comma separated list of extra attributes to include on the "
|
||||
"returned view. The supported attributes are are `filters` and "
|
||||
"`sortings`. "
|
||||
"returned view. The supported attributes are `filters`, "
|
||||
"`sortings` and `decorations`. "
|
||||
"For example `include=filters,sortings` will add the attributes "
|
||||
"`filters` and `sortings` to every returned view, containing "
|
||||
"a list of the views filters and sortings respectively."
|
||||
|
@ -311,14 +335,18 @@ class ViewView(APIView):
|
|||
UserNotInGroup: ERROR_USER_NOT_IN_GROUP,
|
||||
}
|
||||
)
|
||||
@allowed_includes("filters", "sortings")
|
||||
def get(self, request, view_id, filters, sortings):
|
||||
@allowed_includes("filters", "sortings", "decorations")
|
||||
def get(self, request, view_id, filters, sortings, decorations):
|
||||
"""Selects a single view and responds with a serialized version."""
|
||||
|
||||
view = ViewHandler().get_view(view_id)
|
||||
view.table.database.group.has_user(request.user, raise_error=True)
|
||||
serializer = view_type_registry.get_serializer(
|
||||
view, ViewSerializer, filters=filters, sortings=sortings
|
||||
view,
|
||||
ViewSerializer,
|
||||
filters=filters,
|
||||
sortings=sortings,
|
||||
decorations=decorations,
|
||||
)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
@ -336,8 +364,8 @@ class ViewView(APIView):
|
|||
type=OpenApiTypes.STR,
|
||||
description=(
|
||||
"A comma separated list of extra attributes to include on the "
|
||||
"returned view. The supported attributes are `filters` and "
|
||||
"`sortings`. "
|
||||
"returned view. The supported attributes are `filters`, "
|
||||
"`sortings` and `decorations`. "
|
||||
"For example `include=filters,sortings` will add the attributes "
|
||||
"`filters` and `sortings` to every returned view, containing "
|
||||
"a list of the views filters and sortings respectively."
|
||||
|
@ -375,8 +403,8 @@ class ViewView(APIView):
|
|||
UserNotInGroup: ERROR_USER_NOT_IN_GROUP,
|
||||
}
|
||||
)
|
||||
@allowed_includes("filters", "sortings")
|
||||
def patch(self, request, view_id, filters, sortings):
|
||||
@allowed_includes("filters", "sortings", "decorations")
|
||||
def patch(self, request, view_id, filters, sortings, decorations):
|
||||
"""Updates the view if the user belongs to the group."""
|
||||
|
||||
view = (
|
||||
|
@ -397,7 +425,11 @@ class ViewView(APIView):
|
|||
view = ViewHandler().update_view(request.user, view, **data)
|
||||
|
||||
serializer = view_type_registry.get_serializer(
|
||||
view, ViewSerializer, filters=filters, sortings=sortings
|
||||
view,
|
||||
ViewSerializer,
|
||||
filters=filters,
|
||||
sortings=sortings,
|
||||
decorations=decorations,
|
||||
)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
@ -748,6 +780,253 @@ class ViewFilterView(APIView):
|
|||
return Response(status=204)
|
||||
|
||||
|
||||
class ViewDecorationsView(APIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="view_id",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.INT,
|
||||
description=(
|
||||
"Returns only decoration of the view given to the provided "
|
||||
"value."
|
||||
),
|
||||
)
|
||||
],
|
||||
tags=["Database table view decorations"],
|
||||
operation_id="list_database_table_view_decorations",
|
||||
description=(
|
||||
"Lists all decorations of the view related to the provided `view_id` if "
|
||||
"the user has access to the related database's group. A view can have "
|
||||
"multiple decorations. View decorators can be used to decorate rows. This "
|
||||
"can, for example, be used to change the border or background color of "
|
||||
"a row if it matches certain conditions."
|
||||
),
|
||||
responses={
|
||||
200: ViewDecorationSerializer(many=True),
|
||||
400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]),
|
||||
404: get_error_schema(["ERROR_VIEW_DOES_NOT_EXIST"]),
|
||||
},
|
||||
)
|
||||
@map_exceptions(
|
||||
{
|
||||
ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST,
|
||||
UserNotInGroup: ERROR_USER_NOT_IN_GROUP,
|
||||
}
|
||||
)
|
||||
def get(self, request, view_id):
|
||||
"""
|
||||
Responds with a list of serialized decorations that belong to the view
|
||||
if the user has access to that group.
|
||||
"""
|
||||
|
||||
view = ViewHandler().get_view(view_id)
|
||||
view.table.database.group.has_user(request.user, raise_error=True)
|
||||
decorations = ViewDecoration.objects.filter(view=view)
|
||||
serializer = ViewDecorationSerializer(decorations, many=True)
|
||||
return Response(serializer.data)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="view_id",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.INT,
|
||||
description="Creates a decoration for the view related to the given "
|
||||
"value.",
|
||||
)
|
||||
],
|
||||
tags=["Database table view decorations"],
|
||||
operation_id="create_database_table_view_decoration",
|
||||
description=(
|
||||
"Creates a new decoration for the view related to the provided `view_id` "
|
||||
"parameter if the authorized user has access to the related database's "
|
||||
"group."
|
||||
),
|
||||
request=CreateViewDecorationSerializer(),
|
||||
responses={
|
||||
200: ViewDecorationSerializer(),
|
||||
400: get_error_schema(
|
||||
[
|
||||
"ERROR_USER_NOT_IN_GROUP",
|
||||
"ERROR_REQUEST_BODY_VALIDATION",
|
||||
]
|
||||
),
|
||||
404: get_error_schema(["ERROR_VIEW_DOES_NOT_EXIST"]),
|
||||
},
|
||||
)
|
||||
@transaction.atomic
|
||||
@validate_body(CreateViewDecorationSerializer)
|
||||
@map_exceptions(
|
||||
{
|
||||
ViewDoesNotExist: ERROR_VIEW_DOES_NOT_EXIST,
|
||||
UserNotInGroup: ERROR_USER_NOT_IN_GROUP,
|
||||
ViewDecorationNotSupported: ERROR_VIEW_DECORATION_NOT_SUPPORTED,
|
||||
}
|
||||
)
|
||||
def post(self, request, data, view_id):
|
||||
"""Creates a new decoration for the provided view."""
|
||||
|
||||
view_handler = ViewHandler()
|
||||
view = view_handler.get_view(view_id)
|
||||
|
||||
group = view.table.database.group
|
||||
group.has_user(request.user, raise_error=True)
|
||||
|
||||
# We can safely assume the field exists because the
|
||||
# CreateViewDecorationSerializer has already checked that.
|
||||
view_decoration = view_handler.create_decoration(
|
||||
view,
|
||||
data["type"],
|
||||
data.get("value_provider_type", None),
|
||||
data.get("value_provider_conf", None),
|
||||
user=request.user,
|
||||
)
|
||||
|
||||
serializer = ViewDecorationSerializer(view_decoration)
|
||||
return Response(serializer.data)
|
||||
|
||||
|
||||
class ViewDecorationView(APIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="view_decoration_id",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.INT,
|
||||
description=("Returns the view decoration related to the provided id."),
|
||||
)
|
||||
],
|
||||
tags=["Database table view decorations"],
|
||||
operation_id="get_database_table_view_decoration",
|
||||
description=(
|
||||
"Returns the existing view decoration if the current user has access to "
|
||||
"the related database's group."
|
||||
),
|
||||
responses={
|
||||
200: ViewDecorationSerializer(),
|
||||
400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]),
|
||||
404: get_error_schema(["ERROR_VIEW_DECORATION_DOES_NOT_EXIST"]),
|
||||
},
|
||||
)
|
||||
@map_exceptions(
|
||||
{
|
||||
UserNotInGroup: ERROR_USER_NOT_IN_GROUP,
|
||||
ViewDecorationDoesNotExist: ERROR_VIEW_DECORATION_DOES_NOT_EXIST,
|
||||
}
|
||||
)
|
||||
def get(self, request, view_decoration_id):
|
||||
"""Selects a single decoration and responds with a serialized version."""
|
||||
|
||||
view_decoration = ViewHandler().get_decoration(view_decoration_id)
|
||||
|
||||
group = view_decoration.view.table.database.group
|
||||
group.has_user(request.user, raise_error=True)
|
||||
|
||||
serializer = ViewDecorationSerializer(view_decoration)
|
||||
return Response(serializer.data)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="view_decoration_id",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.INT,
|
||||
description="Updates the view decoration related to the provided value.",
|
||||
)
|
||||
],
|
||||
tags=["Database table view decorations"],
|
||||
operation_id="update_database_table_view_decoration",
|
||||
description=(
|
||||
"Updates the existing decoration if the authorized user has access to the "
|
||||
"related database's group."
|
||||
),
|
||||
request=UpdateViewDecorationSerializer(),
|
||||
responses={
|
||||
200: ViewDecorationSerializer(),
|
||||
400: get_error_schema(
|
||||
[
|
||||
"ERROR_USER_NOT_IN_GROUP",
|
||||
]
|
||||
),
|
||||
404: get_error_schema(["ERROR_VIEW_DECORATION_DOES_NOT_EXIST"]),
|
||||
},
|
||||
)
|
||||
@transaction.atomic
|
||||
@validate_body(UpdateViewDecorationSerializer)
|
||||
@map_exceptions(
|
||||
{
|
||||
ViewDecorationDoesNotExist: ERROR_VIEW_DECORATION_DOES_NOT_EXIST,
|
||||
UserNotInGroup: ERROR_USER_NOT_IN_GROUP,
|
||||
}
|
||||
)
|
||||
def patch(self, request, data, view_decoration_id):
|
||||
"""Updates the view decoration if the user belongs to the group."""
|
||||
|
||||
handler = ViewHandler()
|
||||
view_decoration = handler.get_decoration(
|
||||
view_decoration_id,
|
||||
base_queryset=ViewDecoration.objects.select_for_update(),
|
||||
)
|
||||
|
||||
group = view_decoration.view.table.database.group
|
||||
group.has_user(request.user, raise_error=True)
|
||||
|
||||
view_decoration = handler.update_decoration(
|
||||
view_decoration, user=request.user, **data
|
||||
)
|
||||
|
||||
serializer = ViewDecorationSerializer(view_decoration)
|
||||
return Response(serializer.data)
|
||||
|
||||
@extend_schema(
|
||||
parameters=[
|
||||
OpenApiParameter(
|
||||
name="view_decoration_id",
|
||||
location=OpenApiParameter.PATH,
|
||||
type=OpenApiTypes.INT,
|
||||
description="Deletes the decoration related to the provided value.",
|
||||
)
|
||||
],
|
||||
tags=["Database table view decorations"],
|
||||
operation_id="delete_database_table_view_decoration",
|
||||
description=(
|
||||
"Deletes the existing decoration if the authorized user has access to the "
|
||||
"related database's group."
|
||||
),
|
||||
responses={
|
||||
204: None,
|
||||
400: get_error_schema(["ERROR_USER_NOT_IN_GROUP"]),
|
||||
404: get_error_schema(["ERROR_VIEW_decoration_DOES_NOT_EXIST"]),
|
||||
},
|
||||
)
|
||||
@transaction.atomic
|
||||
@map_exceptions(
|
||||
{
|
||||
ViewDecorationDoesNotExist: ERROR_VIEW_DECORATION_DOES_NOT_EXIST,
|
||||
UserNotInGroup: ERROR_USER_NOT_IN_GROUP,
|
||||
}
|
||||
)
|
||||
def delete(self, request, view_decoration_id):
|
||||
"""Deletes an existing decoration if the user belongs to the group."""
|
||||
|
||||
view_decoration = ViewHandler().get_decoration(view_decoration_id)
|
||||
|
||||
group = view_decoration.view.table.database.group
|
||||
group.has_user(request.user, raise_error=True)
|
||||
|
||||
ViewHandler().delete_decoration(
|
||||
view_decoration,
|
||||
user=request.user,
|
||||
)
|
||||
|
||||
return Response(status=204)
|
||||
|
||||
|
||||
class ViewSortingsView(APIView):
|
||||
permission_classes = (IsAuthenticated,)
|
||||
|
||||
|
|
|
@ -0,0 +1,85 @@
|
|||
# Generated by Django 3.2.12 on 2022-04-21 12:31
|
||||
|
||||
import baserow.core.mixins
|
||||
from django.db import migrations, models
|
||||
import django.db.models.deletion
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
("database", "0066_airtableimportjob"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.CreateModel(
|
||||
name="ViewDecoration",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"type",
|
||||
models.CharField(
|
||||
help_text=(
|
||||
"The decorator type. This is then interpreted by "
|
||||
"the frontend to display the decoration."
|
||||
),
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_provider_type",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default="",
|
||||
help_text=(
|
||||
"The value provider for that gives the value to "
|
||||
"the decorator."
|
||||
),
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"value_provider_conf",
|
||||
models.JSONField(
|
||||
default=dict,
|
||||
help_text="The configuration consumed by the value provider.",
|
||||
),
|
||||
),
|
||||
(
|
||||
"order",
|
||||
models.SmallIntegerField(
|
||||
default=32767,
|
||||
help_text=(
|
||||
"The position of the decorator has within the "
|
||||
"view, lowest first. If there is another decorator "
|
||||
"with the same order value then the decorator with "
|
||||
"the lowest id must be shown first."
|
||||
),
|
||||
),
|
||||
),
|
||||
(
|
||||
"view",
|
||||
models.ForeignKey(
|
||||
help_text=(
|
||||
"The view to which the decoration applies. Each view "
|
||||
"can have his own decorations."
|
||||
),
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
to="database.view",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"ordering": ("order", "id"),
|
||||
},
|
||||
bases=(baserow.core.mixins.OrderableMixin, models.Model),
|
||||
),
|
||||
]
|
|
@ -119,6 +119,14 @@ class GridViewAggregationDoesNotSupportField(Exception):
|
|||
)
|
||||
|
||||
|
||||
class ViewDecorationDoesNotExist(Exception):
|
||||
"""Raised when trying to get a view decoration that does not exist."""
|
||||
|
||||
|
||||
class ViewDecorationNotSupported(Exception):
|
||||
"""Raised when the view type does not support aggregations."""
|
||||
|
||||
|
||||
class FormViewFieldTypeIsNotSupported(Exception):
|
||||
"""Raised when someone tries to enable an unsupported form view field."""
|
||||
|
||||
|
|
|
@ -8,6 +8,7 @@ from django.core.exceptions import FieldDoesNotExist, ValidationError
|
|||
from django.core.cache import cache
|
||||
from django.db import models as django_models
|
||||
from django.db.models import F, Count
|
||||
from django.db.models.query import QuerySet
|
||||
from django.contrib.auth.models import AbstractUser
|
||||
|
||||
from baserow.contrib.database.fields.exceptions import FieldNotInTable
|
||||
|
@ -37,8 +38,10 @@ from .exceptions import (
|
|||
ViewDoesNotSupportFieldOptions,
|
||||
FieldAggregationNotSupported,
|
||||
CannotShareViewTypeError,
|
||||
ViewDecorationNotSupported,
|
||||
ViewDecorationDoesNotExist,
|
||||
)
|
||||
from .models import View, ViewFilter, ViewSort
|
||||
from .models import View, ViewDecoration, ViewFilter, ViewSort
|
||||
from .registries import (
|
||||
view_type_registry,
|
||||
view_filter_type_registry,
|
||||
|
@ -55,6 +58,9 @@ from .signals import (
|
|||
view_sort_created,
|
||||
view_sort_updated,
|
||||
view_sort_deleted,
|
||||
view_decoration_created,
|
||||
view_decoration_updated,
|
||||
view_decoration_deleted,
|
||||
view_field_options_updated,
|
||||
)
|
||||
from .validators import EMPTY_VALUES
|
||||
|
@ -878,6 +884,150 @@ class ViewHandler:
|
|||
self, view_sort_id=view_sort_id, view_sort=view_sort, user=user
|
||||
)
|
||||
|
||||
def create_decoration(
|
||||
self,
|
||||
view: View,
|
||||
type: str,
|
||||
value_provider_type: str,
|
||||
value_provider_conf: Dict[str, Any],
|
||||
user: Union["AbstractUser", None] = None,
|
||||
) -> ViewDecoration:
|
||||
"""
|
||||
Creates a new view decoration.
|
||||
|
||||
:param view: The view for which the filter needs to be created.
|
||||
:param type: The type of the decorator.
|
||||
:param value_provider_type: The value provider that provides the value to the
|
||||
decorator.
|
||||
:param value_provider_conf: The configuration used by the value provider to
|
||||
compute the values for the decorator.
|
||||
:param user: Optional user who have created the decoration.
|
||||
:return: The created view decoration instance.
|
||||
"""
|
||||
|
||||
# Check if view supports decoration
|
||||
view_type = view_type_registry.get_by_model(view.specific_class)
|
||||
if not view_type.can_decorate:
|
||||
raise ViewDecorationNotSupported(
|
||||
f"Decoration is not supported for {view_type.type} views."
|
||||
)
|
||||
|
||||
last_order = ViewDecoration.get_last_order(view)
|
||||
|
||||
view_decoration = ViewDecoration.objects.create(
|
||||
view=view,
|
||||
type=type,
|
||||
value_provider_type=value_provider_type,
|
||||
value_provider_conf=value_provider_conf,
|
||||
order=last_order,
|
||||
)
|
||||
|
||||
view_decoration_created.send(self, view_decoration=view_decoration, user=user)
|
||||
|
||||
return view_decoration
|
||||
|
||||
def get_decoration(
|
||||
self,
|
||||
view_decoration_id: int,
|
||||
base_queryset: QuerySet = None,
|
||||
) -> ViewDecoration:
|
||||
"""
|
||||
Returns an existing view decoration with the given id.
|
||||
|
||||
:param view_decoration_id: The id of the view decoration.
|
||||
:param base_queryset: The base queryset from where to select the view decoration
|
||||
object from. This can for example be used to do a `select_related`.
|
||||
:raises ViewDecorationDoesNotExist: The requested view decoration does not
|
||||
exists.
|
||||
:return: The requested view decoration instance.
|
||||
"""
|
||||
|
||||
if base_queryset is None:
|
||||
base_queryset = ViewDecoration.objects
|
||||
|
||||
try:
|
||||
view_decoration = base_queryset.select_related(
|
||||
"view__table__database__group"
|
||||
).get(pk=view_decoration_id)
|
||||
except ViewDecoration.DoesNotExist:
|
||||
raise ViewDecorationDoesNotExist(
|
||||
f"The view decoration with id {view_decoration_id} does not exist."
|
||||
)
|
||||
|
||||
if TrashHandler.item_has_a_trashed_parent(
|
||||
view_decoration.view.table, check_item_also=True
|
||||
):
|
||||
raise ViewDecorationDoesNotExist(
|
||||
f"The view decoration with id {view_decoration_id} does not exist."
|
||||
)
|
||||
|
||||
return view_decoration
|
||||
|
||||
def update_decoration(
|
||||
self,
|
||||
view_decoration: ViewDecoration,
|
||||
user: Union["AbstractUser", None] = None,
|
||||
**kwargs: Dict[str, Any],
|
||||
) -> ViewDecoration:
|
||||
"""
|
||||
Updates the values of an existing view decoration.
|
||||
|
||||
:param view_decoration: The view decoration that needs to be updated.
|
||||
:param kwargs: The values that need to be updated, allowed values are
|
||||
`type`, `value_provider_type`, `value_provider_conf` and `order`.
|
||||
:param user: Optional user who have updated the decoration.
|
||||
:raises ViewDecorationDoesNotExist: The requested view decoration does not
|
||||
exists.
|
||||
:return: The updated view decoration instance.
|
||||
"""
|
||||
|
||||
decoration_type = kwargs.get("type", view_decoration.type)
|
||||
value_provider_type = kwargs.get(
|
||||
"value_provider_type", view_decoration.value_provider_type
|
||||
)
|
||||
value_provider_conf = kwargs.get(
|
||||
"value_provider_conf", view_decoration.value_provider_conf
|
||||
)
|
||||
order = kwargs.get("order", view_decoration.order)
|
||||
|
||||
view_decoration.type = decoration_type
|
||||
view_decoration.value_provider_type = value_provider_type
|
||||
view_decoration.value_provider_conf = value_provider_conf
|
||||
view_decoration.order = order
|
||||
view_decoration.save()
|
||||
|
||||
view_decoration_updated.send(self, view_decoration=view_decoration, user=user)
|
||||
|
||||
return view_decoration
|
||||
|
||||
def delete_decoration(
|
||||
self,
|
||||
view_decoration: ViewDecoration,
|
||||
user: Union["AbstractUser", None] = None,
|
||||
):
|
||||
"""
|
||||
Deletes an existing view decoration.
|
||||
|
||||
:param view_decoration: The view decoration instance that needs to be deleted.
|
||||
:param user: Optional user who have deleted the decoration.
|
||||
:raises ViewDecorationDoesNotExist: The requested view decoration does not
|
||||
exists.
|
||||
"""
|
||||
|
||||
group = view_decoration.view.table.database.group
|
||||
group.has_user(user, raise_error=True)
|
||||
|
||||
view_decoration_id = view_decoration.id
|
||||
view_decoration.delete()
|
||||
|
||||
view_decoration_deleted.send(
|
||||
self,
|
||||
view_decoration_id=view_decoration_id,
|
||||
view_decoration=view_decoration,
|
||||
view_filter=view_decoration,
|
||||
user=user,
|
||||
)
|
||||
|
||||
def get_queryset(
|
||||
self,
|
||||
view,
|
||||
|
|
|
@ -209,6 +209,48 @@ class ViewFilter(ParentFieldTrashableModelMixin, models.Model):
|
|||
return view_filter_type_registry.get(self.type).get_preload_values(self)
|
||||
|
||||
|
||||
class ViewDecoration(OrderableMixin, models.Model):
|
||||
view = models.ForeignKey(
|
||||
View,
|
||||
on_delete=models.CASCADE,
|
||||
help_text="The view to which the decoration applies. Each view can have his own "
|
||||
"decorations.",
|
||||
)
|
||||
type = models.CharField(
|
||||
max_length=255,
|
||||
help_text=(
|
||||
"The decorator type. This is then interpreted by the frontend to "
|
||||
"display the decoration."
|
||||
),
|
||||
)
|
||||
value_provider_type = models.CharField(
|
||||
max_length=255,
|
||||
blank=True,
|
||||
default="",
|
||||
help_text="The value provider for that gives the value to the decorator.",
|
||||
)
|
||||
value_provider_conf = models.JSONField(
|
||||
default=dict,
|
||||
help_text="The configuration consumed by the value provider.",
|
||||
)
|
||||
# The default value is the maximum value of the small integer field because a newly
|
||||
# created decoration must always be last.
|
||||
order = models.SmallIntegerField(
|
||||
default=32767,
|
||||
help_text="The position of the decorator has within the view, lowest first. If "
|
||||
"there is another decorator with the same order value then the decorator "
|
||||
"with the lowest id must be shown first.",
|
||||
)
|
||||
|
||||
@classmethod
|
||||
def get_last_order(cls, view):
|
||||
queryset = ViewDecoration.objects.filter(view=view)
|
||||
return cls.get_highest_order_of_queryset(queryset) + 1
|
||||
|
||||
class Meta:
|
||||
ordering = ("order", "id")
|
||||
|
||||
|
||||
class ViewSort(ParentFieldTrashableModelMixin, models.Model):
|
||||
view = models.ForeignKey(
|
||||
View,
|
||||
|
|
|
@ -94,6 +94,12 @@ class ViewType(
|
|||
to compute fields aggregation for this view type.
|
||||
"""
|
||||
|
||||
can_decorate = False
|
||||
"""
|
||||
Indicates if the view supports decoration. If not, it will not be possible
|
||||
to create decoration for this view type.
|
||||
"""
|
||||
|
||||
can_share = False
|
||||
"""
|
||||
Indicates if the view supports being shared via a public link.
|
||||
|
@ -187,6 +193,18 @@ class ViewType(
|
|||
for sort in view.viewsort_set.all()
|
||||
]
|
||||
|
||||
if self.can_decorate:
|
||||
serialized["decorations"] = [
|
||||
{
|
||||
"id": deco.id,
|
||||
"type": deco.type,
|
||||
"value_provider_type": deco.value_provider_type,
|
||||
"value_provider_conf": deco.value_provider_conf,
|
||||
"order": deco.order,
|
||||
}
|
||||
for deco in view.viewdecoration_set.all()
|
||||
]
|
||||
|
||||
if self.can_share:
|
||||
serialized["public"] = view.public
|
||||
|
||||
|
@ -217,18 +235,22 @@ class ViewType(
|
|||
:rtype: View
|
||||
"""
|
||||
|
||||
from .models import ViewFilter, ViewSort
|
||||
from .models import ViewFilter, ViewSort, ViewDecoration
|
||||
|
||||
if "database_views" not in id_mapping:
|
||||
id_mapping["database_views"] = {}
|
||||
id_mapping["database_view_filters"] = {}
|
||||
id_mapping["database_view_sortings"] = {}
|
||||
id_mapping["database_view_decorations"] = {}
|
||||
|
||||
serialized_copy = serialized_values.copy()
|
||||
view_id = serialized_copy.pop("id")
|
||||
serialized_copy.pop("type")
|
||||
filters = serialized_copy.pop("filters") if self.can_filter else []
|
||||
sortings = serialized_copy.pop("sortings") if self.can_sort else []
|
||||
decorations = (
|
||||
serialized_copy.pop("decorations", []) if self.can_decorate else []
|
||||
)
|
||||
view = self.model_class.objects.create(table=table, **serialized_copy)
|
||||
id_mapping["database_views"][view_id] = view.id
|
||||
|
||||
|
@ -253,8 +275,8 @@ class ViewType(
|
|||
] = view_filter_object.id
|
||||
|
||||
if self.can_sort:
|
||||
for view_sort in sortings:
|
||||
view_sort_copy = view_sort.copy()
|
||||
for view_decoration in sortings:
|
||||
view_sort_copy = view_decoration.copy()
|
||||
view_sort_id = view_sort_copy.pop("id")
|
||||
view_sort_copy["field_id"] = id_mapping["database_fields"][
|
||||
view_sort_copy["field_id"]
|
||||
|
@ -262,6 +284,39 @@ class ViewType(
|
|||
view_sort_object = ViewSort.objects.create(view=view, **view_sort_copy)
|
||||
id_mapping["database_view_sortings"][view_sort_id] = view_sort_object.id
|
||||
|
||||
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:
|
||||
view_decoration_copy = view_decoration.copy()
|
||||
view_decoration_id = view_decoration_copy.pop("id")
|
||||
|
||||
# Deeply update field ids to new one in value provider conf
|
||||
view_decoration_copy["value_provider_conf"] = _update_field_id(
|
||||
view_decoration_copy["value_provider_conf"]
|
||||
)
|
||||
|
||||
view_decoration_object = ViewDecoration.objects.create(
|
||||
view=view, **view_decoration_copy
|
||||
)
|
||||
id_mapping["database_view_decorations"][
|
||||
view_decoration_id
|
||||
] = view_decoration_object.id
|
||||
|
||||
return view
|
||||
|
||||
def get_visible_fields_and_model(self, view):
|
||||
|
|
|
@ -18,6 +18,11 @@ view_filter_deleted = Signal()
|
|||
view_sort_created = Signal()
|
||||
view_sort_updated = Signal()
|
||||
view_sort_deleted = Signal()
|
||||
|
||||
view_decoration_created = Signal()
|
||||
view_decoration_updated = Signal()
|
||||
view_decoration_deleted = Signal()
|
||||
|
||||
view_field_options_updated = Signal()
|
||||
|
||||
|
||||
|
|
|
@ -47,6 +47,7 @@ class GridViewType(ViewType):
|
|||
field_options_serializer_class = GridViewFieldOptionsSerializer
|
||||
can_aggregate_field = True
|
||||
can_share = True
|
||||
can_decorate = True
|
||||
when_shared_publicly_requires_realtime_events = True
|
||||
|
||||
api_exceptions_map = {
|
||||
|
|
|
@ -9,6 +9,7 @@ from baserow.contrib.database.api.views.serializers import (
|
|||
ViewSerializer,
|
||||
ViewFilterSerializer,
|
||||
ViewSortSerializer,
|
||||
ViewDecorationSerializer,
|
||||
)
|
||||
|
||||
|
||||
|
@ -24,6 +25,7 @@ def view_created(sender, view, user, **kwargs):
|
|||
ViewSerializer,
|
||||
filters=True,
|
||||
sortings=True,
|
||||
decorations=True,
|
||||
).data,
|
||||
},
|
||||
getattr(user, "web_socket_id", None),
|
||||
|
@ -43,11 +45,13 @@ def view_updated(sender, view, user, **kwargs):
|
|||
"view": view_type_registry.get_serializer(
|
||||
view,
|
||||
ViewSerializer,
|
||||
# We do not want to broad cast the filters and sortings every time
|
||||
# the view changes. There are separate views and handlers for them
|
||||
# We do not want to broad cast the filters, decorations and sortings
|
||||
# every time the view changes.
|
||||
# There are separate views and handlers for them
|
||||
# each will broad cast their own message.
|
||||
filters=False,
|
||||
sortings=False,
|
||||
decorations=False,
|
||||
).data,
|
||||
},
|
||||
getattr(user, "web_socket_id", None),
|
||||
|
@ -174,6 +178,55 @@ def view_sort_deleted(sender, view_sort_id, view_sort, user, **kwargs):
|
|||
)
|
||||
|
||||
|
||||
@receiver(view_signals.view_decoration_created)
|
||||
def view_decoration_created(sender, view_decoration, user, **kwargs):
|
||||
table_page_type = page_registry.get("table")
|
||||
transaction.on_commit(
|
||||
lambda: table_page_type.broadcast(
|
||||
{
|
||||
"type": "view_decoration_created",
|
||||
"view_decoration": ViewDecorationSerializer(view_decoration).data,
|
||||
},
|
||||
getattr(user, "web_socket_id", None),
|
||||
table_id=view_decoration.view.table_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@receiver(view_signals.view_decoration_updated)
|
||||
def view_decoration_updated(sender, view_decoration, user, **kwargs):
|
||||
table_page_type = page_registry.get("table")
|
||||
transaction.on_commit(
|
||||
lambda: table_page_type.broadcast(
|
||||
{
|
||||
"type": "view_decoration_updated",
|
||||
"view_decoration_id": view_decoration.id,
|
||||
"view_decoration": ViewDecorationSerializer(view_decoration).data,
|
||||
},
|
||||
getattr(user, "web_socket_id", None),
|
||||
table_id=view_decoration.view.table_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@receiver(view_signals.view_decoration_deleted)
|
||||
def view_decoration_deleted(
|
||||
sender, view_decoration_id, view_decoration, user, **kwargs
|
||||
):
|
||||
table_page_type = page_registry.get("table")
|
||||
transaction.on_commit(
|
||||
lambda: table_page_type.broadcast(
|
||||
{
|
||||
"type": "view_decoration_deleted",
|
||||
"view_id": view_decoration.view_id,
|
||||
"view_decoration_id": view_decoration_id,
|
||||
},
|
||||
getattr(user, "web_socket_id", None),
|
||||
table_id=view_decoration.view.table_id,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@receiver(view_signals.view_field_options_updated)
|
||||
def view_field_options_updated(sender, view, user, **kwargs):
|
||||
table_page_type = page_registry.get("table")
|
||||
|
|
|
@ -8,6 +8,7 @@ from baserow.contrib.database.views.models import (
|
|||
FormViewFieldOptions,
|
||||
ViewFilter,
|
||||
ViewSort,
|
||||
ViewDecoration,
|
||||
)
|
||||
|
||||
|
||||
|
@ -114,3 +115,21 @@ class ViewFixtures:
|
|||
kwargs["order"] = "ASC"
|
||||
|
||||
return ViewSort.objects.create(**kwargs)
|
||||
|
||||
def create_view_decoration(self, user=None, **kwargs):
|
||||
if "view" not in kwargs:
|
||||
kwargs["view"] = self.create_grid_view(user)
|
||||
|
||||
if "type" not in kwargs:
|
||||
kwargs["type"] = "left_border_color"
|
||||
|
||||
if "value_provider_type" not in kwargs:
|
||||
kwargs["value_provider_type"] = "single_select_color"
|
||||
|
||||
if "value_provider_conf" not in kwargs:
|
||||
kwargs["value_provider_conf"] = {}
|
||||
|
||||
if "order" not in kwargs:
|
||||
kwargs["order"] = 0
|
||||
|
||||
return ViewDecoration.objects.create(**kwargs)
|
||||
|
|
|
@ -274,6 +274,7 @@ def test_to_baserow_database_export():
|
|||
"filters_disabled": False,
|
||||
"filters": [],
|
||||
"sortings": [],
|
||||
"decorations": [],
|
||||
"public": False,
|
||||
"field_options": [],
|
||||
}
|
||||
|
|
|
@ -54,6 +54,7 @@ def test_create_form_view(api_client, data_fixture):
|
|||
assert form.submit_action_redirect_url == ""
|
||||
assert "filters" not in response_json
|
||||
assert "sortings" not in response_json
|
||||
assert "decorations" not in response_json
|
||||
|
||||
response = api_client.post(
|
||||
reverse("api:database:views:list", kwargs={"table_id": table.id}),
|
||||
|
|
|
@ -1547,9 +1547,10 @@ def test_create_grid_view(api_client, data_fixture):
|
|||
assert response_json["filters_disabled"] == grid.filters_disabled
|
||||
assert "filters" not in response_json
|
||||
assert "sortings" not in response_json
|
||||
assert "decorations" not in response_json
|
||||
|
||||
response = api_client.post(
|
||||
"{}?include=filters,sortings".format(
|
||||
"{}?include=filters,sortings,decorations".format(
|
||||
reverse("api:database:views:list", kwargs={"table_id": table.id})
|
||||
),
|
||||
{
|
||||
|
@ -1569,6 +1570,7 @@ def test_create_grid_view(api_client, data_fixture):
|
|||
assert response_json["filters_disabled"] is False
|
||||
assert response_json["filters"] == []
|
||||
assert response_json["sortings"] == []
|
||||
assert response_json["decorations"] == []
|
||||
|
||||
response = api_client.post(
|
||||
"{}".format(reverse("api:database:views:list", kwargs={"table_id": table.id})),
|
||||
|
@ -1584,6 +1586,7 @@ def test_create_grid_view(api_client, data_fixture):
|
|||
assert response_json["filters_disabled"] is False
|
||||
assert "filters" not in response_json
|
||||
assert "sortings" not in response_json
|
||||
assert "decorations" not in response_json
|
||||
|
||||
# Can't create a public non sharable view.
|
||||
response = api_client.post(
|
||||
|
@ -1665,6 +1668,7 @@ def test_update_grid_view(api_client, data_fixture):
|
|||
assert response_json["filters_disabled"]
|
||||
assert "filters" not in response_json
|
||||
assert "sortings" not in response_json
|
||||
assert "decorations" not in response_json
|
||||
|
||||
view.refresh_from_db()
|
||||
assert view.filter_type == "OR"
|
||||
|
@ -1673,7 +1677,7 @@ def test_update_grid_view(api_client, data_fixture):
|
|||
filter_1 = data_fixture.create_view_filter(view=view)
|
||||
url = reverse("api:database:views:item", kwargs={"view_id": view.id})
|
||||
response = api_client.patch(
|
||||
"{}?include=filters,sortings".format(url),
|
||||
"{}?include=filters,sortings,decorations".format(url),
|
||||
{"filter_type": "AND"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
|
@ -1685,6 +1689,7 @@ def test_update_grid_view(api_client, data_fixture):
|
|||
assert response_json["filters_disabled"] is True
|
||||
assert response_json["filters"][0]["id"] == filter_1.id
|
||||
assert response_json["sortings"] == []
|
||||
assert response_json["decorations"] == []
|
||||
|
||||
# Can't make a non sharable view public.
|
||||
response = api_client.patch(
|
||||
|
|
|
@ -0,0 +1,439 @@
|
|||
import pytest
|
||||
|
||||
from rest_framework.status import (
|
||||
HTTP_200_OK,
|
||||
HTTP_204_NO_CONTENT,
|
||||
HTTP_400_BAD_REQUEST,
|
||||
HTTP_404_NOT_FOUND,
|
||||
)
|
||||
|
||||
from django.shortcuts import reverse
|
||||
|
||||
from baserow.contrib.database.views.models import ViewDecoration
|
||||
from baserow.contrib.database.views.registries import (
|
||||
view_type_registry,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_view_decorations(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table_1 = data_fixture.create_database_table(user=user)
|
||||
table_2 = data_fixture.create_database_table()
|
||||
view_1 = data_fixture.create_grid_view(table=table_1, order=1)
|
||||
view_2 = data_fixture.create_grid_view(table=table_1, order=2)
|
||||
view_3 = data_fixture.create_grid_view(table=table_2, order=1)
|
||||
decoration_1 = data_fixture.create_view_decoration(view=view_1, order=1)
|
||||
decoration_2 = data_fixture.create_view_decoration(
|
||||
view=view_1,
|
||||
type="background_color",
|
||||
value_provider_type="conditionnal_color",
|
||||
order=2,
|
||||
)
|
||||
data_fixture.create_view_decoration(view=view_2, order=3)
|
||||
data_fixture.create_view_decoration(view=view_3, order=4)
|
||||
|
||||
response = api_client.get(
|
||||
reverse("api:database:views:list_decorations", kwargs={"view_id": view_3.id}),
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_USER_NOT_IN_GROUP"
|
||||
|
||||
response = api_client.get(
|
||||
reverse("api:database:views:list_decorations", kwargs={"view_id": 999999}),
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
assert response.json()["error"] == "ERROR_VIEW_DOES_NOT_EXIST"
|
||||
|
||||
response = api_client.get(
|
||||
reverse("api:database:views:list_decorations", kwargs={"view_id": view_1.id}),
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
|
||||
assert len(response_json) == 2
|
||||
assert response_json[0]["id"] == decoration_1.id
|
||||
assert response_json[0]["view"] == view_1.id
|
||||
assert response_json[0]["type"] == decoration_1.type
|
||||
assert response_json[0]["value_provider_type"] == decoration_1.value_provider_type
|
||||
assert response_json[1]["id"] == decoration_2.id
|
||||
assert response_json[1]["type"] == decoration_2.type
|
||||
assert response_json[1]["value_provider_type"] == decoration_2.value_provider_type
|
||||
|
||||
response = api_client.delete(
|
||||
reverse("api:groups:item", kwargs={"group_id": table_1.database.group.id}),
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_204_NO_CONTENT
|
||||
response = api_client.get(
|
||||
reverse("api:database:views:list_decorations", kwargs={"view_id": view_1.id}),
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
assert response.json()["error"] == "ERROR_VIEW_DOES_NOT_EXIST"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_create_view_decoration(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table_1 = data_fixture.create_database_table(user=user)
|
||||
table_2 = data_fixture.create_database_table()
|
||||
view_1 = data_fixture.create_grid_view(table=table_1)
|
||||
view_2 = data_fixture.create_grid_view(table=table_2)
|
||||
|
||||
response = api_client.post(
|
||||
reverse("api:database:views:list_decorations", kwargs={"view_id": view_2.id}),
|
||||
{
|
||||
"type": "left_border_color",
|
||||
"value_provider_type": "single_select_color",
|
||||
"value_provider_conf": {},
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_USER_NOT_IN_GROUP"
|
||||
|
||||
response = api_client.post(
|
||||
reverse("api:database:views:list_decorations", kwargs={"view_id": 99999}),
|
||||
{
|
||||
"type": "left_border_color",
|
||||
"value_provider_type": "single_select_color",
|
||||
"value_provider_conf": {},
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
assert response.json()["error"] == "ERROR_VIEW_DOES_NOT_EXIST"
|
||||
|
||||
grid_view_type = view_type_registry.get("grid")
|
||||
grid_view_type.can_decorate = False
|
||||
response = api_client.post(
|
||||
reverse("api:database:views:list_decorations", kwargs={"view_id": view_1.id}),
|
||||
{
|
||||
"type": "left_border_color",
|
||||
"value_provider_type": "single_select_color",
|
||||
"value_provider_conf": {},
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response_json["error"] == "ERROR_VIEW_DECORATION_NOT_SUPPORTED"
|
||||
grid_view_type.can_decorate = True
|
||||
|
||||
response = api_client.post(
|
||||
reverse("api:database:views:list_decorations", kwargs={"view_id": view_1.id}),
|
||||
{
|
||||
"type": "left_border_color",
|
||||
"value_provider_type": "single_select_color",
|
||||
"value_provider_conf": {"field": 1},
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert ViewDecoration.objects.all().count() == 1
|
||||
first = ViewDecoration.objects.all().first()
|
||||
assert response_json["id"] == first.id
|
||||
assert response_json["view"] == view_1.id
|
||||
assert response_json["type"] == first.type
|
||||
assert response_json["value_provider_type"] == first.value_provider_type
|
||||
assert response_json["value_provider_conf"] == first.value_provider_conf
|
||||
|
||||
response = api_client.post(
|
||||
reverse("api:database:views:list_decorations", kwargs={"view_id": view_1.id}),
|
||||
{"type": "background_color"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert response_json["type"] == "background_color"
|
||||
assert response_json["value_provider_type"] == ""
|
||||
assert response_json["value_provider_conf"] == {}
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_view_decoration(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
decoration_1 = data_fixture.create_view_decoration(user=user)
|
||||
decoration_2 = data_fixture.create_view_decoration()
|
||||
|
||||
response = api_client.get(
|
||||
reverse(
|
||||
"api:database:views:decoration_item",
|
||||
kwargs={"view_decoration_id": decoration_2.id},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_USER_NOT_IN_GROUP"
|
||||
|
||||
response = api_client.get(
|
||||
reverse(
|
||||
"api:database:views:decoration_item", kwargs={"view_decoration_id": 99999}
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
assert response.json()["error"] == "ERROR_VIEW_DECORATION_DOES_NOT_EXIST"
|
||||
|
||||
response = api_client.get(
|
||||
reverse(
|
||||
"api:database:views:decoration_item",
|
||||
kwargs={"view_decoration_id": decoration_1.id},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert ViewDecoration.objects.all().count() == 2
|
||||
first = ViewDecoration.objects.get(pk=decoration_1.id)
|
||||
assert response_json["id"] == first.id
|
||||
assert response_json["view"] == first.view_id
|
||||
assert response_json["type"] == first.type
|
||||
assert response_json["value_provider_type"] == first.value_provider_type
|
||||
assert response_json["value_provider_conf"] == first.value_provider_conf
|
||||
assert response_json["order"] == first.order
|
||||
|
||||
response = api_client.delete(
|
||||
reverse(
|
||||
"api:groups:item",
|
||||
kwargs={"group_id": decoration_1.view.table.database.group.id},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_204_NO_CONTENT
|
||||
|
||||
response = api_client.get(
|
||||
reverse(
|
||||
"api:database:views:decoration_item",
|
||||
kwargs={"view_decoration_id": decoration_1.id},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
assert response.json()["error"] == "ERROR_VIEW_DECORATION_DOES_NOT_EXIST"
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_update_view_decoration(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
decoration_1 = data_fixture.create_view_decoration(user=user)
|
||||
decoration_2 = data_fixture.create_view_decoration()
|
||||
|
||||
response = api_client.patch(
|
||||
reverse(
|
||||
"api:database:views:decoration_item",
|
||||
kwargs={"view_decoration_id": decoration_2.id},
|
||||
),
|
||||
{"type": "left_border_color"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_USER_NOT_IN_GROUP"
|
||||
|
||||
response = api_client.patch(
|
||||
reverse(
|
||||
"api:database:views:decoration_item", kwargs={"view_decoration_id": 9999}
|
||||
),
|
||||
{"type": "left_border_color"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
assert response.json()["error"] == "ERROR_VIEW_DECORATION_DOES_NOT_EXIST"
|
||||
|
||||
response = api_client.patch(
|
||||
reverse(
|
||||
"api:database:views:decoration_item",
|
||||
kwargs={"view_decoration_id": decoration_1.id},
|
||||
),
|
||||
{
|
||||
"type": "background_color",
|
||||
"value_provider_type": "conditional_color",
|
||||
"value_provider_conf": {"test": True},
|
||||
"order": 25,
|
||||
},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
assert ViewDecoration.objects.all().count() == 2
|
||||
first = ViewDecoration.objects.get(pk=decoration_1.id)
|
||||
assert first.type == "background_color"
|
||||
assert first.value_provider_type == "conditional_color"
|
||||
assert first.value_provider_conf == {"test": True}
|
||||
assert first.order == 25
|
||||
assert response_json["id"] == first.id
|
||||
assert response_json["view"] == first.view_id
|
||||
assert response_json["type"] == first.type
|
||||
assert response_json["value_provider_type"] == first.value_provider_type
|
||||
assert response_json["value_provider_conf"] == first.value_provider_conf
|
||||
assert response_json["order"] == first.order
|
||||
|
||||
response = api_client.patch(
|
||||
reverse(
|
||||
"api:database:views:decoration_item",
|
||||
kwargs={"view_decoration_id": decoration_1.id},
|
||||
),
|
||||
{"type": "left_border_color"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
first = ViewDecoration.objects.get(pk=decoration_1.id)
|
||||
|
||||
assert first.type == "left_border_color"
|
||||
assert first.value_provider_type == "conditional_color"
|
||||
assert first.value_provider_conf == {"test": True}
|
||||
assert first.order == 25
|
||||
assert response_json["type"] == first.type
|
||||
assert response_json["value_provider_type"] == first.value_provider_type
|
||||
assert response_json["value_provider_conf"] == first.value_provider_conf
|
||||
assert response_json["order"] == first.order
|
||||
|
||||
response = api_client.patch(
|
||||
reverse(
|
||||
"api:database:views:decoration_item",
|
||||
kwargs={"view_decoration_id": decoration_1.id},
|
||||
),
|
||||
{"value_provider_type": "single_select_color"},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
first = ViewDecoration.objects.get(pk=decoration_1.id)
|
||||
|
||||
assert first.type == "left_border_color"
|
||||
assert first.value_provider_type == "single_select_color"
|
||||
assert response_json["type"] == first.type
|
||||
assert response_json["value_provider_type"] == first.value_provider_type
|
||||
|
||||
response = api_client.patch(
|
||||
reverse(
|
||||
"api:database:views:decoration_item",
|
||||
kwargs={"view_decoration_id": decoration_1.id},
|
||||
),
|
||||
{"value_provider_conf": {"answer": 42}},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
first = ViewDecoration.objects.get(pk=decoration_1.id)
|
||||
|
||||
assert first.value_provider_conf == {"answer": 42}
|
||||
assert response_json["value_provider_conf"] == first.value_provider_conf
|
||||
|
||||
response = api_client.patch(
|
||||
reverse(
|
||||
"api:database:views:decoration_item",
|
||||
kwargs={"view_decoration_id": decoration_1.id},
|
||||
),
|
||||
{"order": 42},
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
response_json = response.json()
|
||||
assert response.status_code == HTTP_200_OK
|
||||
first = ViewDecoration.objects.get(pk=decoration_1.id)
|
||||
|
||||
assert first.order == 42
|
||||
assert response_json["order"] == first.order
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_delete_view_decoration(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
decoration_1 = data_fixture.create_view_decoration(user=user)
|
||||
decoration_2 = data_fixture.create_view_decoration()
|
||||
|
||||
response = api_client.delete(
|
||||
reverse(
|
||||
"api:database:views:decoration_item",
|
||||
kwargs={"view_decoration_id": decoration_2.id},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_400_BAD_REQUEST
|
||||
assert response.json()["error"] == "ERROR_USER_NOT_IN_GROUP"
|
||||
|
||||
response = api_client.delete(
|
||||
reverse(
|
||||
"api:database:views:decoration_item", kwargs={"view_decoration_id": 9999}
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_404_NOT_FOUND
|
||||
assert response.json()["error"] == "ERROR_VIEW_DECORATION_DOES_NOT_EXIST"
|
||||
|
||||
response = api_client.delete(
|
||||
reverse(
|
||||
"api:database:views:decoration_item",
|
||||
kwargs={"view_decoration_id": decoration_1.id},
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == 204
|
||||
assert ViewDecoration.objects.all().count() == 1
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_list_views_including_decorations(api_client, data_fixture):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
table_1 = data_fixture.create_database_table(user=user)
|
||||
table_2 = data_fixture.create_database_table()
|
||||
view_1 = data_fixture.create_grid_view(table=table_1, order=1)
|
||||
view_2 = data_fixture.create_grid_view(table=table_1, order=2)
|
||||
view_3 = data_fixture.create_grid_view(table=table_2, order=1)
|
||||
decoration_1 = data_fixture.create_view_decoration(view=view_1, order=0)
|
||||
decoration_2 = data_fixture.create_view_decoration(
|
||||
view=view_1, type="background_color", order=1
|
||||
)
|
||||
decoration_3 = data_fixture.create_view_decoration(view=view_2)
|
||||
data_fixture.create_view_decoration(view=view_3, type="background_color")
|
||||
|
||||
response = api_client.get(
|
||||
"{}".format(
|
||||
reverse("api:database:views:list", kwargs={"table_id": table_1.id})
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
response_json = response.json()
|
||||
assert len(response_json) == 2
|
||||
assert "decorations" not in response_json[0]
|
||||
assert "decorations" not in response_json[1]
|
||||
|
||||
response = api_client.get(
|
||||
"{}?include=decorations".format(
|
||||
reverse("api:database:views:list", kwargs={"table_id": table_1.id})
|
||||
),
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
assert response.status_code == HTTP_200_OK
|
||||
response_json = response.json()
|
||||
|
||||
assert len(response_json[0]["decorations"]) == 2
|
||||
|
||||
assert response_json[0]["decorations"][0]["id"] == decoration_1.id
|
||||
assert response_json[0]["decorations"][0]["view"] == view_1.id
|
||||
assert response_json[0]["decorations"][0]["type"] == decoration_1.type
|
||||
assert (
|
||||
response_json[0]["decorations"][0]["value_provider_type"]
|
||||
== decoration_1.value_provider_type
|
||||
)
|
||||
assert response_json[0]["decorations"][1]["id"] == decoration_2.id
|
||||
assert len(response_json[1]["decorations"]) == 1
|
||||
assert response_json[1]["decorations"][0]["id"] == decoration_3.id
|
|
@ -123,10 +123,11 @@ def test_get_view(api_client, data_fixture):
|
|||
assert not response_json["filters_disabled"]
|
||||
assert "filters" not in response_json
|
||||
assert "sortings" not in response_json
|
||||
assert "decorations" not in response_json
|
||||
|
||||
url = reverse("api:database:views:item", kwargs={"view_id": view.id})
|
||||
response = api_client.get(
|
||||
"{}?include=filters,sortings".format(url),
|
||||
"{}?include=filters,sortings,decorations".format(url),
|
||||
format="json",
|
||||
HTTP_AUTHORIZATION=f"JWT {token}",
|
||||
)
|
||||
|
@ -140,6 +141,7 @@ def test_get_view(api_client, data_fixture):
|
|||
assert response_json["filters"][0]["type"] == view_filter.type
|
||||
assert response_json["filters"][0]["value"] == view_filter.value
|
||||
assert response_json["sortings"] == []
|
||||
assert response_json["decorations"] == []
|
||||
|
||||
response = api_client.delete(
|
||||
reverse("api:groups:item", kwargs={"group_id": view.table.database.group.id}),
|
||||
|
|
|
@ -34,6 +34,18 @@ def test_import_export_grid_view(data_fixture):
|
|||
)
|
||||
view_sort = data_fixture.create_view_sort(view=grid_view, field=field, order="ASC")
|
||||
|
||||
view_decoration = data_fixture.create_view_decoration(
|
||||
view=grid_view,
|
||||
value_provider_conf={
|
||||
"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}}
|
||||
|
||||
grid_view_type = view_type_registry.get("grid")
|
||||
|
@ -61,6 +73,23 @@ def test_import_export_grid_view(data_fixture):
|
|||
assert imported_field.id == imported_view_sort.field_id
|
||||
assert view_sort.order == imported_view_sort.order
|
||||
|
||||
imported_view_decoration = imported_grid_view.viewdecoration_set.all().first()
|
||||
assert view_decoration.id != imported_view_decoration.id
|
||||
assert view_decoration.type == imported_view_decoration.type
|
||||
assert (
|
||||
view_decoration.value_provider_type
|
||||
== imported_view_decoration.value_provider_type
|
||||
)
|
||||
assert imported_view_decoration.value_provider_conf == {
|
||||
"field_id": imported_field.id,
|
||||
"other": [
|
||||
{"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
|
||||
|
||||
imported_field_options = imported_grid_view.get_field_options()
|
||||
imported_field_option = imported_field_options[0]
|
||||
assert field_option.id != imported_field_option.id
|
||||
|
|
|
@ -174,6 +174,62 @@ def test_view_sort_deleted(mock_broadcast_to_channel_group, data_fixture):
|
|||
assert args[0][1]["view_sort_id"] == view_sort_id
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_view_decoration_created(mock_broadcast_to_channel_group, data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
table = data_fixture.create_database_table(user=user)
|
||||
field = data_fixture.create_text_field(table=table)
|
||||
view = data_fixture.create_grid_view(user=user, table=table)
|
||||
view_decoration = ViewHandler().create_decoration(
|
||||
user=user,
|
||||
view=view,
|
||||
type="type",
|
||||
value_provider_type="value_provider_type",
|
||||
value_provider_conf={},
|
||||
)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_called_once()
|
||||
args = mock_broadcast_to_channel_group.delay.call_args
|
||||
assert args[0][0] == f"table-{table.id}"
|
||||
assert args[0][1]["type"] == "view_decoration_created"
|
||||
assert args[0][1]["view_decoration"]["id"] == view_decoration.id
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_view_decoration_updated(mock_broadcast_to_channel_group, data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
view_decoration = data_fixture.create_view_decoration(user=user)
|
||||
view_decoration = ViewHandler().update_decoration(
|
||||
user=user, view_decoration=view_decoration, type="new_type"
|
||||
)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_called_once()
|
||||
args = mock_broadcast_to_channel_group.delay.call_args
|
||||
assert args[0][0] == f"table-{view_decoration.view.table.id}"
|
||||
assert args[0][1]["type"] == "view_decoration_updated"
|
||||
assert args[0][1]["view_decoration_id"] == view_decoration.id
|
||||
assert args[0][1]["view_decoration"]["id"] == view_decoration.id
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_view_decoration_deleted(mock_broadcast_to_channel_group, data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
view_decoration = data_fixture.create_view_decoration(user=user)
|
||||
view_id = view_decoration.view.id
|
||||
view_decoration_id = view_decoration.id
|
||||
ViewHandler().delete_decoration(user=user, view_decoration=view_decoration)
|
||||
|
||||
mock_broadcast_to_channel_group.delay.assert_called_once()
|
||||
args = mock_broadcast_to_channel_group.delay.call_args
|
||||
assert args[0][0] == f"table-{view_decoration.view.table.id}"
|
||||
assert args[0][1]["type"] == "view_decoration_deleted"
|
||||
assert args[0][1]["view_id"] == view_id
|
||||
assert args[0][1]["view_decoration_id"] == view_decoration_id
|
||||
|
||||
|
||||
@pytest.mark.django_db(transaction=True)
|
||||
@patch("baserow.ws.registries.broadcast_to_channel_group")
|
||||
def test_view_field_options_updated(mock_broadcast_to_channel_group, data_fixture):
|
||||
|
|
|
@ -158,6 +158,9 @@ are subscribed to the page.
|
|||
* `view_sort_created`
|
||||
* `view_sort_updated`
|
||||
* `view_sort_deleted`
|
||||
* `view_decoration_created`
|
||||
* `view_decoration_updated`
|
||||
* `view_decoration_deleted`
|
||||
* `view_field_options_updated`
|
||||
* `views_reordered`
|
||||
|
||||
|
|
|
@ -8,7 +8,7 @@
|
|||
:table="table"
|
||||
:fields="selectFields"
|
||||
:value="value"
|
||||
@input="$emit('update', { field: $event })"
|
||||
@input="$emit('update', { field_id: $event })"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -46,7 +46,7 @@ export default {
|
|||
return this.fields.filter(({ type }) => type === 'single_select')
|
||||
},
|
||||
value() {
|
||||
return this.options && this.options.field
|
||||
return this.options && this.options.field_id
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -35,14 +35,14 @@ export class SingleSelectColorValueProviderType extends DecoratorValueProviderTy
|
|||
}
|
||||
|
||||
getValue({ options, fields, row }) {
|
||||
const value = row[`field_${options.field}`]
|
||||
const value = row[`field_${options.field_id}`]
|
||||
return value?.color || ''
|
||||
}
|
||||
|
||||
getDefaultConfiguration({ fields }) {
|
||||
const firstSelectField = fields.find(({ type }) => type === 'single_select')
|
||||
return {
|
||||
field: firstSelectField?.id,
|
||||
field_id: firstSelectField?.id,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,9 +1,13 @@
|
|||
.value-provider-list.value-provider-list--row {
|
||||
.value-provider-list--row {
|
||||
display: flex;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.value-provider-list--read-only {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.value-provider-list__item {
|
||||
position: relative;
|
||||
padding: 12px;
|
||||
|
@ -47,4 +51,9 @@
|
|||
margin-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
.value-provider-list--read-only &:hover {
|
||||
cursor: default;
|
||||
background-color: transparent;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,14 +1,20 @@
|
|||
<template>
|
||||
<div class="value-provider-list" :class="`value-provider-list--${direction}`">
|
||||
<div
|
||||
class="value-provider-list"
|
||||
:class="{
|
||||
[`value-provider-list--${direction}`]: true,
|
||||
'value-provider-list--read-only': readOnly,
|
||||
}"
|
||||
>
|
||||
<div
|
||||
v-for="valueProviderType in availableValueProviderTypes"
|
||||
:key="valueProviderType.getType()"
|
||||
class="value-provider-list__item"
|
||||
:class="{
|
||||
'value-provider-list__item--selected':
|
||||
valueProviderType.getType() === decoration.value_provider,
|
||||
valueProviderType.getType() === decoration.value_provider_type,
|
||||
}"
|
||||
@click="$emit('select', valueProviderType.getType())"
|
||||
@click="!readOnly && $emit('select', valueProviderType.getType())"
|
||||
>
|
||||
<DecoratorValueProviderItem :value-provider-type="valueProviderType" />
|
||||
</div>
|
||||
|
@ -31,6 +37,11 @@ export default {
|
|||
required: false,
|
||||
default: 'column',
|
||||
},
|
||||
readOnly: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
availableValueProviderTypes() {
|
||||
|
|
|
@ -17,11 +17,11 @@
|
|||
<ViewDecoratorItem :decorator-type="dec.decoratorType" />
|
||||
</div>
|
||||
<div
|
||||
v-show="dec.decoration.value_provider"
|
||||
v-show="dec.decoration.value_provider_type"
|
||||
class="decorator-context__decorator-header-select"
|
||||
>
|
||||
<Picker
|
||||
v-if="dec.decoration.value_provider"
|
||||
v-if="dec.decoration.value_provider_type"
|
||||
:icon="dec.valueProviderType.getIconClass()"
|
||||
:name="dec.valueProviderType.getName()"
|
||||
@select="selectValueProvider(dec.decoration, $event)"
|
||||
|
@ -60,7 +60,7 @@
|
|||
:view="view"
|
||||
:table="table"
|
||||
:fields="allFields"
|
||||
:read-only="readOnly"
|
||||
:read-only="readOnly || dec.decoration._.loading"
|
||||
:options="dec.decoration.value_provider_conf"
|
||||
@update="updateDecorationOptions(dec.decoration, $event)"
|
||||
/>
|
||||
|
@ -68,6 +68,7 @@
|
|||
v-else
|
||||
:decoration="dec.decoration"
|
||||
:direction="'row'"
|
||||
:read-only="readOnly || dec.decoration._.loading"
|
||||
@select="selectValueProvider(dec.decoration, $event)"
|
||||
/>
|
||||
</div>
|
||||
|
@ -152,10 +153,10 @@ export default {
|
|||
decoration.type
|
||||
)
|
||||
|
||||
if (decoration.value_provider) {
|
||||
if (decoration.value_provider_type) {
|
||||
deco.valueProviderType = this.$registry.get(
|
||||
'decoratorValueProvider',
|
||||
decoration.value_provider
|
||||
decoration.value_provider_type
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -188,7 +189,7 @@ export default {
|
|||
await this.$store.dispatch('view/updateDecoration', {
|
||||
view: this.view,
|
||||
values: {
|
||||
value_provider: valueProviderType.getType(),
|
||||
value_provider_type: valueProviderType.getType(),
|
||||
value_provider_conf: valueProviderType.getDefaultConfiguration({
|
||||
view: this.view,
|
||||
fields: this.fields,
|
||||
|
@ -200,7 +201,8 @@ export default {
|
|||
async addDecoration(decoratorType) {
|
||||
const decoration = {
|
||||
type: decoratorType.getType(),
|
||||
value_provider: null,
|
||||
value_provider_type: '',
|
||||
value_provider_conf: {},
|
||||
}
|
||||
await this.$store.dispatch('view/createDecoration', {
|
||||
view: this.view,
|
||||
|
|
|
@ -70,7 +70,7 @@ export default {
|
|||
},
|
||||
augmentedDecorations() {
|
||||
return this.view.decorations
|
||||
.filter(({ value_provider: valPro }) => valPro)
|
||||
.filter(({ value_provider_type: valPro }) => valPro)
|
||||
.map((decoration) => {
|
||||
const deco = { decoration }
|
||||
|
||||
|
@ -84,7 +84,7 @@ export default {
|
|||
|
||||
deco.valueProviderType = this.$registry.get(
|
||||
'decoratorValueProvider',
|
||||
decoration.value_provider
|
||||
decoration.value_provider_type
|
||||
)
|
||||
deco.propsFn = (row) => {
|
||||
return {
|
||||
|
|
|
@ -58,7 +58,13 @@ export class DecoratorValueProviderType extends Registerable {
|
|||
}
|
||||
|
||||
/**
|
||||
* Should return the component that allow to configure the value provider.
|
||||
* Returns the component that allows the user to configure the value provider.
|
||||
* This component is responsible for creating the `value_provider_conf` object.
|
||||
*
|
||||
* If you want to reference fields in this object, you must use the key name
|
||||
* `field_id` to allow import/export to replace automatically any field id by the new
|
||||
* field id. `field_id`s can be anywhere in this object and this object can be as
|
||||
* deep as you need.
|
||||
*/
|
||||
getFormComponent() {
|
||||
throw new Error(
|
||||
|
@ -75,6 +81,20 @@ export class DecoratorValueProviderType extends Registerable {
|
|||
return {}
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the value of this provider for the given row considering the configuration.
|
||||
*
|
||||
* @param {array} row the row
|
||||
* @param {object} options the configuration of the value provider
|
||||
* @param {array} fields the array of the fields of the current view
|
||||
*
|
||||
*/
|
||||
getValue({ options, fields, row }) {
|
||||
throw new Error(
|
||||
'Not implemented error. This value provider should return a value.'
|
||||
)
|
||||
}
|
||||
|
||||
getOrder() {
|
||||
return 50
|
||||
}
|
||||
|
|
|
@ -394,6 +394,43 @@ export const registerRealtimeEvents = (realtime) => {
|
|||
}
|
||||
})
|
||||
|
||||
realtime.registerEvent('view_decoration_created', ({ store, app }, data) => {
|
||||
const view = store.getters['view/get'](data.view_decoration.view)
|
||||
if (view !== undefined) {
|
||||
store.dispatch('view/forceCreateDecoration', {
|
||||
view,
|
||||
values: data.view_decoration,
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
realtime.registerEvent('view_decoration_updated', ({ store, app }, data) => {
|
||||
const view = store.getters['view/get'](data.view_decoration.view)
|
||||
if (view !== undefined) {
|
||||
const decoration = view.decorations.find(
|
||||
(deco) => deco.id === data.view_decoration_id
|
||||
)
|
||||
if (decoration !== undefined) {
|
||||
store.dispatch('view/forceUpdateDecoration', {
|
||||
decoration,
|
||||
values: data.view_decoration,
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
realtime.registerEvent('view_decoration_deleted', ({ store, app }, data) => {
|
||||
const view = store.getters['view/get'](data.view_id)
|
||||
if (view !== undefined) {
|
||||
const decoration = view.decorations.find(
|
||||
(deco) => deco.id === data.view_decoration_id
|
||||
)
|
||||
if (decoration !== undefined) {
|
||||
store.dispatch('view/forceDeleteDecoration', { view, decoration })
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
realtime.registerEvent('view_field_options_updated', (context, data) => {
|
||||
const { store, app } = context
|
||||
const view = store.getters['view/get'](data.view_id)
|
||||
|
|
|
@ -1,32 +1,22 @@
|
|||
import { uuid } from '@baserow/modules/core/utils/string'
|
||||
|
||||
/**
|
||||
* For now this is a stub to allow everything to work like we already had the server
|
||||
* endpoints.
|
||||
*/
|
||||
export default (client) => {
|
||||
return {
|
||||
fetchAll(viewId) {
|
||||
// return client.get(`/database/views/${viewId}/decoration/`)
|
||||
return []
|
||||
return client.get(`/database/views/${viewId}/decoration/`)
|
||||
},
|
||||
create(viewId, values) {
|
||||
// return client.post(`/database/views/${viewId}/decorations/`, values)
|
||||
return { data: { ...values, id: uuid() } }
|
||||
return client.post(`/database/views/${viewId}/decorations/`, values)
|
||||
},
|
||||
get(viewDecorationId) {
|
||||
// return client.get(`/database/views/decoration/${viewDecorationId}/`)
|
||||
return {}
|
||||
return client.get(`/database/views/decoration/${viewDecorationId}/`)
|
||||
},
|
||||
update(viewDecorationId, values) {
|
||||
/* return client.patch(
|
||||
return client.patch(
|
||||
`/database/views/decoration/${viewDecorationId}/`,
|
||||
values
|
||||
) */
|
||||
return { data: { ...values } }
|
||||
)
|
||||
},
|
||||
delete(viewDecorationId) {
|
||||
// return client.delete(`/database/views/decoration/${viewDecorationId}/`)
|
||||
return client.delete(`/database/views/decoration/${viewDecorationId}/`)
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
export default (client) => {
|
||||
return {
|
||||
fetchAll(tableId, includeFilters = false, includeSortings = false) {
|
||||
fetchAll(
|
||||
tableId,
|
||||
includeFilters = false,
|
||||
includeSortings = false,
|
||||
includeDecorations = false
|
||||
) {
|
||||
const config = {
|
||||
params: {},
|
||||
}
|
||||
|
@ -14,6 +19,10 @@ export default (client) => {
|
|||
include.push('sortings')
|
||||
}
|
||||
|
||||
if (includeDecorations) {
|
||||
include.push('decorations')
|
||||
}
|
||||
|
||||
if (include.length > 0) {
|
||||
config.params.include = include.join(',')
|
||||
}
|
||||
|
@ -23,7 +32,12 @@ export default (client) => {
|
|||
create(tableId, values) {
|
||||
return client.post(`/database/views/table/${tableId}/`, values)
|
||||
},
|
||||
get(viewId, includeFilters = false, includeSortings = false) {
|
||||
get(
|
||||
viewId,
|
||||
includeFilters = false,
|
||||
includeSortings = false,
|
||||
includeDecorations = false
|
||||
) {
|
||||
const config = {
|
||||
params: {},
|
||||
}
|
||||
|
@ -36,6 +50,10 @@ export default (client) => {
|
|||
include.push('sortings')
|
||||
}
|
||||
|
||||
if (includeDecorations) {
|
||||
include.push('decorations')
|
||||
}
|
||||
|
||||
if (include.length > 0) {
|
||||
config.params.include = include.join(',')
|
||||
}
|
||||
|
|
|
@ -146,7 +146,7 @@ export const mutations = {
|
|||
ADD_DECORATION(state, { view, decoration }) {
|
||||
view.decorations.push({
|
||||
type: null,
|
||||
value_provider: null,
|
||||
value_provider_type: null,
|
||||
value_provider_conf: null,
|
||||
...decoration,
|
||||
})
|
||||
|
@ -220,6 +220,7 @@ export const actions = {
|
|||
const { data } = await ViewService(this.$client).fetchAll(
|
||||
table.id,
|
||||
true,
|
||||
true,
|
||||
true
|
||||
)
|
||||
data.forEach((part, index, d) => {
|
||||
|
|
18
web-frontend/test/fixtures/mockServer.js
vendored
18
web-frontend/test/fixtures/mockServer.js
vendored
|
@ -147,6 +147,24 @@ export class MockServer {
|
|||
}
|
||||
}
|
||||
|
||||
createDecoration(view, values, result) {
|
||||
this.mock
|
||||
.onPost(`/database/views/${view.id}/decorations/`, values)
|
||||
.replyOnce(200, result)
|
||||
}
|
||||
|
||||
updateDecoration(decoration, values, result) {
|
||||
this.mock
|
||||
.onPatch(`/database/views/decoration/${decoration.id}/`, values)
|
||||
.replyOnce(200, result)
|
||||
}
|
||||
|
||||
deleteDecoration(decoration) {
|
||||
this.mock
|
||||
.onDelete(`/database/views/decoration/${decoration.id}/`)
|
||||
.replyOnce(200)
|
||||
}
|
||||
|
||||
resetMockEndpoints() {
|
||||
this.mock.reset()
|
||||
}
|
||||
|
|
|
@ -30,7 +30,7 @@ export class FakeDecoratorType extends ViewDecoratorType {
|
|||
|
||||
export class FakeValueProviderType extends DecoratorValueProviderType {
|
||||
static getType() {
|
||||
return 'fake_value_provider'
|
||||
return 'fake_value_provider_type'
|
||||
}
|
||||
|
||||
getValue({ options, fields, row }) {
|
||||
|
@ -69,7 +69,7 @@ describe('GridViewRows component with decoration', () => {
|
|||
try {
|
||||
store.$registry.unregister(
|
||||
'decoratorValueProvider',
|
||||
'fake_value_provider'
|
||||
'fake_value_provider_type'
|
||||
)
|
||||
} catch {}
|
||||
})
|
||||
|
@ -140,12 +140,12 @@ describe('GridViewRows component with decoration', () => {
|
|||
const { fields, view } = await populateStore([
|
||||
{
|
||||
type: 'fake_decorator',
|
||||
value_provider: 'fake_value_provider',
|
||||
value_provider_type: 'fake_value_provider_type',
|
||||
value_provider_conf: {},
|
||||
},
|
||||
{
|
||||
type: 'fake_decorator',
|
||||
value_provider: 'fake_value_provider',
|
||||
value_provider_type: 'fake_value_provider_type',
|
||||
value_provider_conf: {},
|
||||
},
|
||||
])
|
||||
|
@ -173,12 +173,12 @@ describe('GridViewRows component with decoration', () => {
|
|||
const { fields, view } = await populateStore([
|
||||
{
|
||||
type: 'fake_decorator',
|
||||
value_provider: 'fake_value_provider',
|
||||
value_provider_type: 'fake_value_provider_type',
|
||||
value_provider_conf: {},
|
||||
},
|
||||
{
|
||||
type: 'fake_decorator',
|
||||
value_provider: 'fake_value_provider',
|
||||
value_provider_type: 'fake_value_provider_type',
|
||||
value_provider_conf: {},
|
||||
},
|
||||
])
|
||||
|
@ -218,7 +218,7 @@ describe('GridViewRows component with decoration', () => {
|
|||
const { fields, view } = await populateStore([
|
||||
{
|
||||
type: 'fake_decorator',
|
||||
value_provider: 'fake_value_provider',
|
||||
value_provider_type: 'fake_value_provider_type',
|
||||
value_provider_conf: {},
|
||||
},
|
||||
])
|
||||
|
|
|
@ -39,7 +39,7 @@ export class FakeDecoratorType extends ViewDecoratorType {
|
|||
|
||||
export class FakeValueProviderType extends DecoratorValueProviderType {
|
||||
static getType() {
|
||||
return 'fake_value_provider'
|
||||
return 'fake_value_provider_type'
|
||||
}
|
||||
|
||||
getName() {
|
||||
|
@ -90,7 +90,7 @@ describe('GridViewRows component with decoration', () => {
|
|||
try {
|
||||
store.$registry.unregister(
|
||||
'decoratorValueProvider',
|
||||
'fake_value_provider'
|
||||
'fake_value_provider_type'
|
||||
)
|
||||
} catch {}
|
||||
})
|
||||
|
@ -187,13 +187,15 @@ describe('GridViewRows component with decoration', () => {
|
|||
const { table, fields, view } = await populateStore([
|
||||
{
|
||||
type: 'fake_decorator',
|
||||
value_provider: 'fake_value_provider',
|
||||
value_provider_type: 'fake_value_provider_type',
|
||||
value_provider_conf: {},
|
||||
_: { loading: false },
|
||||
},
|
||||
{
|
||||
type: 'fake_decorator',
|
||||
value_provider: '',
|
||||
value_provider_type: '',
|
||||
value_provider_conf: {},
|
||||
_: { loading: false },
|
||||
},
|
||||
])
|
||||
|
||||
|
|
|
@ -3,10 +3,12 @@ import { TestApp } from '@baserow/test/helpers/testApp'
|
|||
describe('View store - decorator', () => {
|
||||
let testApp = null
|
||||
let store = null
|
||||
let mockServer = null
|
||||
|
||||
beforeEach(() => {
|
||||
testApp = new TestApp()
|
||||
store = testApp.store
|
||||
mockServer = testApp.mockServer
|
||||
})
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -19,31 +21,46 @@ describe('View store - decorator', () => {
|
|||
decorations: [],
|
||||
}
|
||||
|
||||
mockServer.createDecoration(
|
||||
view,
|
||||
{
|
||||
type: 'left_border_color',
|
||||
value_provider_type: 'single_select_color',
|
||||
},
|
||||
{
|
||||
type: 'left_border_color',
|
||||
value_provider_type: 'single_select_color',
|
||||
value_provider_conf: {},
|
||||
order: 1,
|
||||
id: 25,
|
||||
}
|
||||
)
|
||||
|
||||
await store.dispatch('view/createDecoration', {
|
||||
view,
|
||||
values: {
|
||||
type: 'left_border_color',
|
||||
value_provider: 'single_select_color',
|
||||
value_provider_type: 'single_select_color',
|
||||
},
|
||||
})
|
||||
|
||||
expect(view.decorations.length).toBe(1)
|
||||
expect(view.decorations[0].id).toBeDefined()
|
||||
expect(view.decorations[0].type).toBe('left_border_color')
|
||||
expect(view.decorations[0].value_provider).toBe('single_select_color')
|
||||
expect(view.decorations[0].value_provider_type).toBe('single_select_color')
|
||||
expect(view.decorations[0]._).toEqual({ loading: false })
|
||||
|
||||
await store.dispatch('view/forceCreateDecoration', {
|
||||
view,
|
||||
values: {
|
||||
type: 'background_color',
|
||||
value_provider: 'conditional_color',
|
||||
value_provider_type: 'conditional_color',
|
||||
},
|
||||
})
|
||||
|
||||
expect(view.decorations.length).toBe(2)
|
||||
expect(view.decorations[1].type).toBe('background_color')
|
||||
expect(view.decorations[1].value_provider).toBe('conditional_color')
|
||||
expect(view.decorations[1].value_provider_type).toBe('conditional_color')
|
||||
expect(view.decorations[1]._).toEqual({ loading: false })
|
||||
})
|
||||
|
||||
|
@ -51,7 +68,7 @@ describe('View store - decorator', () => {
|
|||
const decoration = {
|
||||
id: 1,
|
||||
type: 'left_border_color',
|
||||
value_provider: 'single_select_color',
|
||||
value_provider_type: 'single_select_color',
|
||||
_: {
|
||||
loading: false,
|
||||
},
|
||||
|
@ -61,6 +78,20 @@ describe('View store - decorator', () => {
|
|||
decorations: [decoration],
|
||||
}
|
||||
|
||||
mockServer.updateDecoration(
|
||||
decoration,
|
||||
{
|
||||
type: 'background_color',
|
||||
},
|
||||
{
|
||||
type: 'left_border_color',
|
||||
value_provider_type: 'background_color',
|
||||
value_provider_conf: {},
|
||||
order: 1,
|
||||
id: 1,
|
||||
}
|
||||
)
|
||||
|
||||
await store.dispatch('view/updateDecoration', {
|
||||
view,
|
||||
decoration,
|
||||
|
@ -71,7 +102,7 @@ describe('View store - decorator', () => {
|
|||
|
||||
expect(view.decorations.length).toBe(1)
|
||||
expect(view.decorations[0].type).toBe('background_color')
|
||||
expect(view.decorations[0].value_provider).toBe('single_select_color')
|
||||
expect(view.decorations[0].value_provider_type).toBe('single_select_color')
|
||||
expect(view.decorations[0]._).toEqual({ loading: false })
|
||||
})
|
||||
|
||||
|
@ -79,7 +110,7 @@ describe('View store - decorator', () => {
|
|||
const decoration = {
|
||||
id: 1,
|
||||
type: 'left_border_color',
|
||||
value_provider: 'single_select_color',
|
||||
value_provider_type: 'single_select_color',
|
||||
_: {
|
||||
loading: false,
|
||||
},
|
||||
|
@ -89,6 +120,8 @@ describe('View store - decorator', () => {
|
|||
decorations: [decoration],
|
||||
}
|
||||
|
||||
mockServer.deleteDecoration(decoration)
|
||||
|
||||
await store.dispatch('view/deleteDecoration', {
|
||||
view,
|
||||
decoration,
|
||||
|
|
Loading…
Add table
Reference in a new issue