mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-14 00:59:06 +00:00
Merge branch '2513-repeat-element-refactor-the-collection-and-container-element-types-into-mixins' into 'develop'
Resolve "Repeat element: refactor the collection and container element types into mixins." Closes #2513 See merge request baserow/baserow!2266
This commit is contained in:
commit
7e04a43b51
7 changed files with 669 additions and 611 deletions
backend
src/baserow/contrib/builder
tests/baserow/contrib/builder/elements
web-frontend/modules/builder
|
@ -12,8 +12,8 @@ from baserow.contrib.builder.data_sources.exceptions import (
|
|||
DataSourceImproperlyConfigured,
|
||||
)
|
||||
from baserow.contrib.builder.data_sources.handler import DataSourceHandler
|
||||
from baserow.contrib.builder.elements.element_types import FormElementType
|
||||
from baserow.contrib.builder.elements.handler import ElementHandler
|
||||
from baserow.contrib.builder.elements.mixins import FormElementTypeMixin
|
||||
from baserow.contrib.builder.elements.models import FormElement
|
||||
from baserow.contrib.builder.workflow_actions.handler import (
|
||||
BuilderWorkflowActionHandler,
|
||||
|
@ -62,7 +62,7 @@ class FormDataProviderType(DataProviderType):
|
|||
"""
|
||||
|
||||
element: Type[FormElement] = ElementHandler().get_element(element_id) # type: ignore
|
||||
element_type: FormElementType = element.get_type() # type: ignore
|
||||
element_type: FormElementTypeMixin = element.get_type() # type: ignore
|
||||
if not element_type.is_valid(element, data_chunk):
|
||||
raise FormDataProviderChunkInvalidException(
|
||||
f"Form data {data_chunk} is invalid for its element."
|
||||
|
|
|
@ -1,33 +1,30 @@
|
|||
import abc
|
||||
from abc import ABC
|
||||
from typing import Any, Dict, List, Optional, Type, TypedDict
|
||||
from typing import Any, Dict, List, Optional, TypedDict
|
||||
|
||||
from django.core.exceptions import ValidationError
|
||||
from django.core.validators import validate_email, validate_integer
|
||||
from django.db.models import IntegerField, QuerySet
|
||||
from django.db.models.functions import Cast
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||||
|
||||
from baserow.api.exceptions import RequestBodyValidationException
|
||||
from baserow.contrib.builder.api.elements.serializers import DropdownOptionSerializer
|
||||
from baserow.contrib.builder.data_sources.handler import DataSourceHandler
|
||||
from baserow.contrib.builder.elements.handler import ElementHandler
|
||||
from baserow.contrib.builder.elements.mixins import (
|
||||
CollectionElementTypeMixin,
|
||||
ContainerElementTypeMixin,
|
||||
FormElementTypeMixin,
|
||||
)
|
||||
from baserow.contrib.builder.elements.models import (
|
||||
INPUT_TEXT_TYPES,
|
||||
WIDTHS,
|
||||
ButtonElement,
|
||||
CheckboxElement,
|
||||
CollectionField,
|
||||
ColumnElement,
|
||||
ContainerElement,
|
||||
DropdownElement,
|
||||
DropdownElementOption,
|
||||
Element,
|
||||
FormContainerElement,
|
||||
FormElement,
|
||||
HeadingElement,
|
||||
HorizontalAlignments,
|
||||
IFrameElement,
|
||||
|
@ -43,7 +40,6 @@ from baserow.contrib.builder.elements.registries import (
|
|||
ElementType,
|
||||
element_type_registry,
|
||||
)
|
||||
from baserow.contrib.builder.elements.signals import elements_moved
|
||||
from baserow.contrib.builder.formula_importer import import_formula
|
||||
from baserow.contrib.builder.pages.handler import PageHandler
|
||||
from baserow.contrib.builder.pages.models import Page
|
||||
|
@ -51,301 +47,8 @@ from baserow.contrib.builder.types import ElementDict
|
|||
from baserow.core.formula.types import BaserowFormula
|
||||
from baserow.core.registry import T
|
||||
|
||||
from .registries import collection_field_type_registry
|
||||
|
||||
|
||||
class ContainerElementType(ElementType, ABC):
|
||||
# Container element types are imported first.
|
||||
import_element_priority = 2
|
||||
|
||||
@property
|
||||
def child_types_allowed(self) -> List[str]:
|
||||
"""
|
||||
Lets you define which children types can be placed inside the container.
|
||||
|
||||
:return: All the allowed children types
|
||||
"""
|
||||
|
||||
return [element_type.type for element_type in element_type_registry.get_all()]
|
||||
|
||||
def get_new_place_in_container(
|
||||
self, container_element: ContainerElement, places_removed: List[str]
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Provides an alternative place that elements can move to when places in the
|
||||
container are removed.
|
||||
|
||||
:param container_element: The container element that has places removed
|
||||
:param places_removed: The places that are being removed
|
||||
:return: The new place in the container the elements can be moved to
|
||||
"""
|
||||
|
||||
return None
|
||||
|
||||
def get_places_in_container_removed(
|
||||
self, values: Dict, instance: ContainerElement
|
||||
) -> List[str]:
|
||||
"""
|
||||
This method defines what elements in the container have been removed preceding
|
||||
an update of hte container element.
|
||||
|
||||
:param values: The new values that are being set
|
||||
:param instance: The current state of the element
|
||||
:return: The places in the container that have been removed
|
||||
"""
|
||||
|
||||
return []
|
||||
|
||||
def apply_order_by_children(self, queryset: QuerySet[Element]) -> QuerySet[Element]:
|
||||
"""
|
||||
Defines the order of the children inside the container.
|
||||
|
||||
:param queryset: The queryset that the order is applied to.
|
||||
:return: A queryset with the order applied to
|
||||
"""
|
||||
|
||||
return queryset.order_by("place_in_container", "order")
|
||||
|
||||
def prepare_value_for_db(
|
||||
self, values: Dict, instance: Optional[ContainerElement] = None
|
||||
):
|
||||
if instance is not None: # This is an update operation
|
||||
places_removed = self.get_places_in_container_removed(values, instance)
|
||||
|
||||
if len(places_removed) > 0:
|
||||
instances_moved = ElementHandler().before_places_in_container_removed(
|
||||
instance, places_removed
|
||||
)
|
||||
|
||||
elements_moved.send(self, page=instance.page, elements=instances_moved)
|
||||
|
||||
return super().prepare_value_for_db(values, instance)
|
||||
|
||||
def validate_place_in_container(
|
||||
self, place_in_container: str, instance: ContainerElement
|
||||
):
|
||||
"""
|
||||
Validate that the place in container being set on a child is valid.
|
||||
|
||||
:param place_in_container: The place in container being set
|
||||
:param instance: The instance of the container element
|
||||
:raises DRFValidationError: If the place in container is invalid
|
||||
"""
|
||||
|
||||
|
||||
class CollectionElementType(ElementType, ABC):
|
||||
allowed_fields = ["data_source", "data_source_id", "items_per_page"]
|
||||
serializer_field_names = ["data_source_id", "fields", "items_per_page"]
|
||||
|
||||
class SerializedDict(ElementDict):
|
||||
data_source_id: int
|
||||
items_per_page: int
|
||||
fields: List[Dict]
|
||||
|
||||
@property
|
||||
def serializer_field_overrides(self):
|
||||
from baserow.contrib.builder.api.elements.serializers import (
|
||||
CollectionFieldSerializer,
|
||||
)
|
||||
|
||||
return {
|
||||
"data_source_id": serializers.IntegerField(
|
||||
allow_null=True,
|
||||
default=None,
|
||||
help_text=TableElement._meta.get_field("data_source").help_text,
|
||||
required=False,
|
||||
),
|
||||
"items_per_page": serializers.IntegerField(
|
||||
default=20,
|
||||
help_text=TableElement._meta.get_field("items_per_page").help_text,
|
||||
required=False,
|
||||
),
|
||||
"fields": CollectionFieldSerializer(many=True, required=False),
|
||||
}
|
||||
|
||||
def prepare_value_for_db(
|
||||
self, values: Dict, instance: Optional[LinkElement] = None
|
||||
):
|
||||
if "data_source_id" in values:
|
||||
data_source_id = values.pop("data_source_id")
|
||||
if data_source_id is not None:
|
||||
data_source = DataSourceHandler().get_data_source(data_source_id)
|
||||
if (
|
||||
not data_source.service
|
||||
or not data_source.service.specific.get_type().returns_list
|
||||
):
|
||||
raise DRFValidationError(
|
||||
f"The data source with ID {data_source_id} doesn't return a "
|
||||
"list."
|
||||
)
|
||||
|
||||
if instance:
|
||||
current_page = PageHandler().get_page(instance.page_id)
|
||||
else:
|
||||
current_page = values["page"]
|
||||
|
||||
if current_page.id != data_source.page_id:
|
||||
raise RequestBodyValidationException(
|
||||
{
|
||||
"data_source_id": [
|
||||
{
|
||||
"detail": "The provided data source doesn't belong "
|
||||
"to the same application.",
|
||||
"code": "invalid_data_source",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
values["data_source"] = data_source
|
||||
else:
|
||||
values["data_source"] = None
|
||||
|
||||
return super().prepare_value_for_db(values, instance)
|
||||
|
||||
def after_create(self, instance, values):
|
||||
default_fields = [
|
||||
{
|
||||
"name": _("Column %(count)s") % {"count": 1},
|
||||
"type": "text",
|
||||
"config": {"value": ""},
|
||||
},
|
||||
{
|
||||
"name": _("Column %(count)s") % {"count": 2},
|
||||
"type": "text",
|
||||
"config": {"value": ""},
|
||||
},
|
||||
{
|
||||
"name": _("Column %(count)s") % {"count": 3},
|
||||
"type": "text",
|
||||
"config": {"value": ""},
|
||||
},
|
||||
]
|
||||
|
||||
fields = values.get("fields", default_fields)
|
||||
|
||||
created_fields = CollectionField.objects.bulk_create(
|
||||
[
|
||||
CollectionField(**field, order=index)
|
||||
for index, field in enumerate(fields)
|
||||
]
|
||||
)
|
||||
instance.fields.add(*created_fields)
|
||||
|
||||
def after_update(self, instance, values):
|
||||
if "fields" in values:
|
||||
# Remove previous fields
|
||||
instance.fields.all().delete()
|
||||
|
||||
created_fields = CollectionField.objects.bulk_create(
|
||||
[
|
||||
CollectionField(**field, order=index)
|
||||
for index, field in enumerate(values["fields"])
|
||||
]
|
||||
)
|
||||
instance.fields.add(*created_fields)
|
||||
|
||||
def before_delete(self, instance):
|
||||
instance.fields.all().delete()
|
||||
|
||||
def serialize_property(self, element: Element, prop_name: str):
|
||||
"""
|
||||
You can customize the behavior of the serialization of a property with this
|
||||
hook.
|
||||
"""
|
||||
|
||||
if prop_name == "fields":
|
||||
return [
|
||||
collection_field_type_registry.get(f.type).export_serialized(f)
|
||||
for f in element.fields.all()
|
||||
]
|
||||
|
||||
return super().serialize_property(element, prop_name)
|
||||
|
||||
def deserialize_property(
|
||||
self,
|
||||
prop_name: str,
|
||||
value: Any,
|
||||
id_mapping: Dict[str, Any],
|
||||
**kwargs,
|
||||
) -> Any:
|
||||
if prop_name == "data_source_id" and value:
|
||||
return id_mapping["builder_data_sources"][value]
|
||||
|
||||
if prop_name == "fields":
|
||||
return [
|
||||
# We need to add the data_source_id for the current row
|
||||
# provider.
|
||||
collection_field_type_registry.get(f["type"]).import_serialized(
|
||||
f, id_mapping, data_source_id=kwargs["data_source_id"]
|
||||
)
|
||||
for f in value
|
||||
]
|
||||
|
||||
return super().deserialize_property(prop_name, value, id_mapping)
|
||||
|
||||
def create_instance_from_serialized(self, serialized_values: Dict[str, Any]):
|
||||
"""Deals with the fields"""
|
||||
|
||||
fields = serialized_values.pop("fields", [])
|
||||
|
||||
instance = super().create_instance_from_serialized(serialized_values)
|
||||
|
||||
# Add the field order
|
||||
for i, f in enumerate(fields):
|
||||
f.order = i
|
||||
|
||||
# Create fields
|
||||
created_fields = CollectionField.objects.bulk_create(fields)
|
||||
|
||||
instance.fields.add(*created_fields)
|
||||
|
||||
return instance
|
||||
|
||||
def import_serialized(
|
||||
self,
|
||||
parent: Any,
|
||||
serialized_values: Dict[str, Any],
|
||||
id_mapping: Dict[str, Any],
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Here we add the data_source_id to the import process to be able to resolve
|
||||
current_record formulas migration.
|
||||
"""
|
||||
|
||||
actual_data_source_id = None
|
||||
if serialized_values.get("data_source_id", None):
|
||||
actual_data_source_id = id_mapping["builder_data_sources"][
|
||||
serialized_values["data_source_id"]
|
||||
]
|
||||
|
||||
return super().import_serialized(
|
||||
parent,
|
||||
serialized_values,
|
||||
id_mapping,
|
||||
data_source_id=actual_data_source_id,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class FormElementType(ElementType):
|
||||
# Form element types are imported second, after containers.
|
||||
import_element_priority = 1
|
||||
|
||||
def is_valid(self, element: Type[FormElement], value: Any) -> bool:
|
||||
"""
|
||||
Given an element and form data value, returns whether it's valid.
|
||||
Used by `FormDataProviderType` to determine if form data is valid.
|
||||
|
||||
:param element: The element we're trying to use form data in.
|
||||
:param value: The form data value, which may be invalid.
|
||||
:return: Whether the value is valid or not for this element.
|
||||
"""
|
||||
|
||||
return not (element.required and not value)
|
||||
|
||||
|
||||
class ColumnElementType(ContainerElementType):
|
||||
class ColumnElementType(ContainerElementTypeMixin, ElementType):
|
||||
"""
|
||||
A column element is a container element that can be used to display other elements
|
||||
in a column.
|
||||
|
@ -421,6 +124,105 @@ class ColumnElementType(ContainerElementType):
|
|||
)
|
||||
|
||||
|
||||
class FormContainerElementType(ContainerElementTypeMixin, ElementType):
|
||||
type = "form_container"
|
||||
model_class = FormContainerElement
|
||||
allowed_fields = [
|
||||
"submit_button_label",
|
||||
"button_color",
|
||||
"reset_initial_values_post_submission",
|
||||
]
|
||||
serializer_field_names = [
|
||||
"submit_button_label",
|
||||
"button_color",
|
||||
"reset_initial_values_post_submission",
|
||||
]
|
||||
|
||||
class SerializedDict(ElementDict):
|
||||
submit_button_label: BaserowFormula
|
||||
button_color: str
|
||||
|
||||
def get_pytest_params(self, pytest_data_fixture) -> Dict[str, Any]:
|
||||
return {"submit_button_label": "'Submit'"}
|
||||
|
||||
@property
|
||||
def serializer_field_overrides(self):
|
||||
from baserow.core.formula.serializers import FormulaSerializerField
|
||||
|
||||
return {
|
||||
"submit_button_label": FormulaSerializerField(
|
||||
help_text=FormContainerElement._meta.get_field(
|
||||
"submit_button_label"
|
||||
).help_text,
|
||||
required=False,
|
||||
allow_blank=True,
|
||||
default="",
|
||||
),
|
||||
"button_color": serializers.CharField(
|
||||
max_length=20,
|
||||
required=False,
|
||||
default="primary",
|
||||
help_text="Button color.",
|
||||
),
|
||||
"reset_initial_values_post_submission": serializers.BooleanField(
|
||||
help_text=FormContainerElement._meta.get_field(
|
||||
"reset_initial_values_post_submission"
|
||||
).help_text,
|
||||
required=False,
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def child_types_allowed(self) -> List[str]:
|
||||
child_types_allowed = []
|
||||
|
||||
for element_type in element_type_registry.get_all():
|
||||
if isinstance(element_type, FormElementTypeMixin):
|
||||
child_types_allowed.append(element_type.type)
|
||||
|
||||
return child_types_allowed
|
||||
|
||||
def import_serialized(self, page, serialized_values, id_mapping):
|
||||
serialized_copy = serialized_values.copy()
|
||||
if serialized_copy["submit_button_label"]:
|
||||
serialized_copy["submit_button_label"] = import_formula(
|
||||
serialized_copy["submit_button_label"], id_mapping
|
||||
)
|
||||
|
||||
return super().import_serialized(page, serialized_copy, id_mapping)
|
||||
|
||||
|
||||
class TableElementType(CollectionElementTypeMixin, ElementType):
|
||||
type = "table"
|
||||
model_class = TableElement
|
||||
|
||||
class SerializedDict(CollectionElementTypeMixin.SerializedDict):
|
||||
button_color: str
|
||||
|
||||
@property
|
||||
def allowed_fields(self):
|
||||
return super().allowed_fields + ["button_color"]
|
||||
|
||||
@property
|
||||
def serializer_field_names(self):
|
||||
return super().serializer_field_names + ["button_color"]
|
||||
|
||||
@property
|
||||
def serializer_field_overrides(self):
|
||||
return {
|
||||
**super().serializer_field_overrides,
|
||||
"button_color": serializers.CharField(
|
||||
max_length=20,
|
||||
required=False,
|
||||
default="primary",
|
||||
help_text="Button color.",
|
||||
),
|
||||
}
|
||||
|
||||
def get_pytest_params(self, pytest_data_fixture) -> Dict[str, Any]:
|
||||
return {"data_source_id": None}
|
||||
|
||||
|
||||
class HeadingElementType(ElementType):
|
||||
"""
|
||||
A simple heading element that can be used to display a title.
|
||||
|
@ -918,7 +720,7 @@ class ImageElementType(ElementType):
|
|||
return super().import_serialized(page, serialized_copy, id_mapping)
|
||||
|
||||
|
||||
class InputElementType(FormElementType, abc.ABC):
|
||||
class InputElementType(FormElementTypeMixin, ElementType, abc.ABC):
|
||||
pass
|
||||
|
||||
|
||||
|
@ -1113,105 +915,6 @@ class ButtonElementType(ElementType):
|
|||
return super().import_serialized(page, serialized_copy, id_mapping)
|
||||
|
||||
|
||||
class TableElementType(CollectionElementType):
|
||||
type = "table"
|
||||
model_class = TableElement
|
||||
|
||||
class SerializedDict(CollectionElementType.SerializedDict):
|
||||
button_color: str
|
||||
|
||||
@property
|
||||
def allowed_fields(self):
|
||||
return super().allowed_fields + ["button_color"]
|
||||
|
||||
@property
|
||||
def serializer_field_names(self):
|
||||
return super().serializer_field_names + ["button_color"]
|
||||
|
||||
@property
|
||||
def serializer_field_overrides(self):
|
||||
return {
|
||||
**super().serializer_field_overrides,
|
||||
"button_color": serializers.CharField(
|
||||
max_length=20,
|
||||
required=False,
|
||||
default="primary",
|
||||
help_text="Button color.",
|
||||
),
|
||||
}
|
||||
|
||||
def get_pytest_params(self, pytest_data_fixture) -> Dict[str, Any]:
|
||||
return {"data_source_id": None}
|
||||
|
||||
|
||||
class FormContainerElementType(ContainerElementType):
|
||||
type = "form_container"
|
||||
model_class = FormContainerElement
|
||||
allowed_fields = [
|
||||
"submit_button_label",
|
||||
"button_color",
|
||||
"reset_initial_values_post_submission",
|
||||
]
|
||||
serializer_field_names = [
|
||||
"submit_button_label",
|
||||
"button_color",
|
||||
"reset_initial_values_post_submission",
|
||||
]
|
||||
|
||||
class SerializedDict(ElementDict):
|
||||
submit_button_label: BaserowFormula
|
||||
button_color: str
|
||||
|
||||
def get_pytest_params(self, pytest_data_fixture) -> Dict[str, Any]:
|
||||
return {"submit_button_label": "'Submit'"}
|
||||
|
||||
@property
|
||||
def serializer_field_overrides(self):
|
||||
from baserow.core.formula.serializers import FormulaSerializerField
|
||||
|
||||
return {
|
||||
"submit_button_label": FormulaSerializerField(
|
||||
help_text=FormContainerElement._meta.get_field(
|
||||
"submit_button_label"
|
||||
).help_text,
|
||||
required=False,
|
||||
allow_blank=True,
|
||||
default="",
|
||||
),
|
||||
"button_color": serializers.CharField(
|
||||
max_length=20,
|
||||
required=False,
|
||||
default="primary",
|
||||
help_text="Button color.",
|
||||
),
|
||||
"reset_initial_values_post_submission": serializers.BooleanField(
|
||||
help_text=FormContainerElement._meta.get_field(
|
||||
"reset_initial_values_post_submission"
|
||||
).help_text,
|
||||
required=False,
|
||||
),
|
||||
}
|
||||
|
||||
@property
|
||||
def child_types_allowed(self) -> List[str]:
|
||||
child_types_allowed = []
|
||||
|
||||
for element_type in element_type_registry.get_all():
|
||||
if isinstance(element_type, FormElementType):
|
||||
child_types_allowed.append(element_type.type)
|
||||
|
||||
return child_types_allowed
|
||||
|
||||
def import_serialized(self, page, serialized_values, id_mapping):
|
||||
serialized_copy = serialized_values.copy()
|
||||
if serialized_copy["submit_button_label"]:
|
||||
serialized_copy["submit_button_label"] = import_formula(
|
||||
serialized_copy["submit_button_label"], id_mapping
|
||||
)
|
||||
|
||||
return super().import_serialized(page, serialized_copy, id_mapping)
|
||||
|
||||
|
||||
class CheckboxElementType(InputElementType):
|
||||
type = "checkbox"
|
||||
model_class = CheckboxElement
|
||||
|
@ -1270,7 +973,7 @@ class CheckboxElementType(InputElementType):
|
|||
}
|
||||
|
||||
|
||||
class DropdownElementType(FormElementType):
|
||||
class DropdownElementType(FormElementTypeMixin, ElementType):
|
||||
type = "dropdown"
|
||||
model_class = DropdownElement
|
||||
allowed_fields = ["label", "default_value", "required", "placeholder"]
|
||||
|
|
320
backend/src/baserow/contrib/builder/elements/mixins.py
Normal file
320
backend/src/baserow/contrib/builder/elements/mixins.py
Normal file
|
@ -0,0 +1,320 @@
|
|||
from typing import Any, Dict, List, Optional, Type
|
||||
|
||||
from django.db.models import QuerySet
|
||||
from django.utils.translation import gettext_lazy as _
|
||||
|
||||
from rest_framework import serializers
|
||||
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||||
|
||||
from baserow.api.exceptions import RequestBodyValidationException
|
||||
from baserow.contrib.builder.data_sources.handler import DataSourceHandler
|
||||
from baserow.contrib.builder.elements.handler import ElementHandler
|
||||
from baserow.contrib.builder.elements.models import (
|
||||
CollectionField,
|
||||
ContainerElement,
|
||||
Element,
|
||||
FormElement,
|
||||
LinkElement,
|
||||
TableElement,
|
||||
)
|
||||
from baserow.contrib.builder.elements.registries import (
|
||||
collection_field_type_registry,
|
||||
element_type_registry,
|
||||
)
|
||||
from baserow.contrib.builder.elements.signals import elements_moved
|
||||
from baserow.contrib.builder.pages.handler import PageHandler
|
||||
from baserow.contrib.builder.types import ElementDict
|
||||
|
||||
|
||||
class ContainerElementTypeMixin:
|
||||
# Container element types are imported first.
|
||||
import_element_priority = 2
|
||||
|
||||
@property
|
||||
def child_types_allowed(self) -> List[str]:
|
||||
"""
|
||||
Lets you define which children types can be placed inside the container.
|
||||
|
||||
:return: All the allowed children types
|
||||
"""
|
||||
|
||||
return [element_type.type for element_type in element_type_registry.get_all()]
|
||||
|
||||
def get_new_place_in_container(
|
||||
self, container_element: ContainerElement, places_removed: List[str]
|
||||
) -> Optional[str]:
|
||||
"""
|
||||
Provides an alternative place that elements can move to when places in the
|
||||
container are removed.
|
||||
|
||||
:param container_element: The container element that has places removed
|
||||
:param places_removed: The places that are being removed
|
||||
:return: The new place in the container the elements can be moved to
|
||||
"""
|
||||
|
||||
return None
|
||||
|
||||
def get_places_in_container_removed(
|
||||
self, values: Dict, instance: ContainerElement
|
||||
) -> List[str]:
|
||||
"""
|
||||
This method defines what elements in the container have been removed preceding
|
||||
an update of hte container element.
|
||||
|
||||
:param values: The new values that are being set
|
||||
:param instance: The current state of the element
|
||||
:return: The places in the container that have been removed
|
||||
"""
|
||||
|
||||
return []
|
||||
|
||||
def apply_order_by_children(self, queryset: QuerySet[Element]) -> QuerySet[Element]:
|
||||
"""
|
||||
Defines the order of the children inside the container.
|
||||
|
||||
:param queryset: The queryset that the order is applied to.
|
||||
:return: A queryset with the order applied to
|
||||
"""
|
||||
|
||||
return queryset.order_by("place_in_container", "order")
|
||||
|
||||
def prepare_value_for_db(
|
||||
self, values: Dict, instance: Optional[ContainerElement] = None
|
||||
):
|
||||
if instance is not None: # This is an update operation
|
||||
places_removed = self.get_places_in_container_removed(values, instance)
|
||||
|
||||
if len(places_removed) > 0:
|
||||
instances_moved = ElementHandler().before_places_in_container_removed(
|
||||
instance, places_removed
|
||||
)
|
||||
|
||||
elements_moved.send(self, page=instance.page, elements=instances_moved)
|
||||
|
||||
return super().prepare_value_for_db(values, instance)
|
||||
|
||||
def validate_place_in_container(
|
||||
self, place_in_container: str, instance: ContainerElement
|
||||
):
|
||||
"""
|
||||
Validate that the place in container being set on a child is valid.
|
||||
|
||||
:param place_in_container: The place in container being set
|
||||
:param instance: The instance of the container element
|
||||
:raises DRFValidationError: If the place in container is invalid
|
||||
"""
|
||||
|
||||
|
||||
class CollectionElementTypeMixin:
|
||||
allowed_fields = ["data_source", "data_source_id", "items_per_page"]
|
||||
serializer_field_names = ["data_source_id", "fields", "items_per_page"]
|
||||
|
||||
class SerializedDict(ElementDict):
|
||||
data_source_id: int
|
||||
items_per_page: int
|
||||
fields: List[Dict]
|
||||
|
||||
@property
|
||||
def serializer_field_overrides(self):
|
||||
from baserow.contrib.builder.api.elements.serializers import (
|
||||
CollectionFieldSerializer,
|
||||
)
|
||||
|
||||
return {
|
||||
"data_source_id": serializers.IntegerField(
|
||||
allow_null=True,
|
||||
default=None,
|
||||
help_text=TableElement._meta.get_field("data_source").help_text,
|
||||
required=False,
|
||||
),
|
||||
"items_per_page": serializers.IntegerField(
|
||||
default=20,
|
||||
help_text=TableElement._meta.get_field("items_per_page").help_text,
|
||||
required=False,
|
||||
),
|
||||
"fields": CollectionFieldSerializer(many=True, required=False),
|
||||
}
|
||||
|
||||
def prepare_value_for_db(
|
||||
self, values: Dict, instance: Optional[LinkElement] = None
|
||||
):
|
||||
if "data_source_id" in values:
|
||||
data_source_id = values.pop("data_source_id")
|
||||
if data_source_id is not None:
|
||||
data_source = DataSourceHandler().get_data_source(data_source_id)
|
||||
if (
|
||||
not data_source.service
|
||||
or not data_source.service.specific.get_type().returns_list
|
||||
):
|
||||
raise DRFValidationError(
|
||||
f"The data source with ID {data_source_id} doesn't return a "
|
||||
"list."
|
||||
)
|
||||
|
||||
if instance:
|
||||
current_page = PageHandler().get_page(instance.page_id)
|
||||
else:
|
||||
current_page = values["page"]
|
||||
|
||||
if current_page.id != data_source.page_id:
|
||||
raise RequestBodyValidationException(
|
||||
{
|
||||
"data_source_id": [
|
||||
{
|
||||
"detail": "The provided data source doesn't belong "
|
||||
"to the same application.",
|
||||
"code": "invalid_data_source",
|
||||
}
|
||||
]
|
||||
}
|
||||
)
|
||||
values["data_source"] = data_source
|
||||
else:
|
||||
values["data_source"] = None
|
||||
|
||||
return super().prepare_value_for_db(values, instance)
|
||||
|
||||
def after_create(self, instance, values):
|
||||
default_fields = [
|
||||
{
|
||||
"name": _("Column %(count)s") % {"count": 1},
|
||||
"type": "text",
|
||||
"config": {"value": ""},
|
||||
},
|
||||
{
|
||||
"name": _("Column %(count)s") % {"count": 2},
|
||||
"type": "text",
|
||||
"config": {"value": ""},
|
||||
},
|
||||
{
|
||||
"name": _("Column %(count)s") % {"count": 3},
|
||||
"type": "text",
|
||||
"config": {"value": ""},
|
||||
},
|
||||
]
|
||||
|
||||
fields = values.get("fields", default_fields)
|
||||
|
||||
created_fields = CollectionField.objects.bulk_create(
|
||||
[
|
||||
CollectionField(**field, order=index)
|
||||
for index, field in enumerate(fields)
|
||||
]
|
||||
)
|
||||
instance.fields.add(*created_fields)
|
||||
|
||||
def after_update(self, instance, values):
|
||||
if "fields" in values:
|
||||
# Remove previous fields
|
||||
instance.fields.all().delete()
|
||||
|
||||
created_fields = CollectionField.objects.bulk_create(
|
||||
[
|
||||
CollectionField(**field, order=index)
|
||||
for index, field in enumerate(values["fields"])
|
||||
]
|
||||
)
|
||||
instance.fields.add(*created_fields)
|
||||
|
||||
def before_delete(self, instance):
|
||||
instance.fields.all().delete()
|
||||
|
||||
def serialize_property(self, element: Element, prop_name: str):
|
||||
"""
|
||||
You can customize the behavior of the serialization of a property with this
|
||||
hook.
|
||||
"""
|
||||
|
||||
if prop_name == "fields":
|
||||
return [
|
||||
collection_field_type_registry.get(f.type).export_serialized(f)
|
||||
for f in element.fields.all()
|
||||
]
|
||||
|
||||
return super().serialize_property(element, prop_name)
|
||||
|
||||
def deserialize_property(
|
||||
self,
|
||||
prop_name: str,
|
||||
value: Any,
|
||||
id_mapping: Dict[str, Any],
|
||||
**kwargs,
|
||||
) -> Any:
|
||||
if prop_name == "data_source_id" and value:
|
||||
return id_mapping["builder_data_sources"][value]
|
||||
|
||||
if prop_name == "fields":
|
||||
return [
|
||||
# We need to add the data_source_id for the current row
|
||||
# provider.
|
||||
collection_field_type_registry.get(f["type"]).import_serialized(
|
||||
f, id_mapping, data_source_id=kwargs["data_source_id"]
|
||||
)
|
||||
for f in value
|
||||
]
|
||||
|
||||
return super().deserialize_property(prop_name, value, id_mapping)
|
||||
|
||||
def create_instance_from_serialized(self, serialized_values: Dict[str, Any]):
|
||||
"""Deals with the fields"""
|
||||
|
||||
fields = serialized_values.pop("fields", [])
|
||||
|
||||
instance = super().create_instance_from_serialized(serialized_values)
|
||||
|
||||
# Add the field order
|
||||
for i, f in enumerate(fields):
|
||||
f.order = i
|
||||
|
||||
# Create fields
|
||||
created_fields = CollectionField.objects.bulk_create(fields)
|
||||
|
||||
instance.fields.add(*created_fields)
|
||||
|
||||
return instance
|
||||
|
||||
def import_serialized(
|
||||
self,
|
||||
parent: Any,
|
||||
serialized_values: Dict[str, Any],
|
||||
id_mapping: Dict[str, Any],
|
||||
**kwargs,
|
||||
):
|
||||
"""
|
||||
Here we add the data_source_id to the import process to be able to resolve
|
||||
current_record formulas migration.
|
||||
"""
|
||||
|
||||
actual_data_source_id = None
|
||||
if (
|
||||
serialized_values.get("data_source_id", None)
|
||||
and "builder_data_sources" in id_mapping
|
||||
):
|
||||
actual_data_source_id = id_mapping["builder_data_sources"][
|
||||
serialized_values["data_source_id"]
|
||||
]
|
||||
|
||||
return super().import_serialized(
|
||||
parent,
|
||||
serialized_values,
|
||||
id_mapping,
|
||||
data_source_id=actual_data_source_id,
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class FormElementTypeMixin:
|
||||
# Form element types are imported second, after containers.
|
||||
import_element_priority = 1
|
||||
|
||||
def is_valid(self, element: Type[FormElement], value: Any) -> bool:
|
||||
"""
|
||||
Given an element and form data value, returns whether it's valid.
|
||||
Used by `FormDataProviderType` to determine if form data is valid.
|
||||
|
||||
:param element: The element we're trying to use form data in.
|
||||
:param value: The form data value, which may be invalid.
|
||||
:return: Whether the value is valid or not for this element.
|
||||
"""
|
||||
|
||||
return not (element.required and not value)
|
|
@ -5,13 +5,15 @@ from rest_framework.exceptions import ValidationError
|
|||
|
||||
from baserow.contrib.builder.elements.element_types import (
|
||||
CheckboxElementType,
|
||||
ContainerElementType,
|
||||
DropdownElementType,
|
||||
FormElementType,
|
||||
IFrameElementType,
|
||||
InputTextElementType,
|
||||
)
|
||||
from baserow.contrib.builder.elements.handler import ElementHandler
|
||||
from baserow.contrib.builder.elements.mixins import (
|
||||
ContainerElementTypeMixin,
|
||||
FormElementTypeMixin,
|
||||
)
|
||||
from baserow.contrib.builder.elements.models import (
|
||||
CheckboxElement,
|
||||
DropdownElementOption,
|
||||
|
@ -232,18 +234,18 @@ def test_element_type_import_element_priority():
|
|||
container_element_types = [
|
||||
element_type
|
||||
for element_type in element_types
|
||||
if isinstance(element_type, ContainerElementType)
|
||||
if isinstance(element_type, ContainerElementTypeMixin)
|
||||
]
|
||||
form_element_types = [
|
||||
element_type
|
||||
for element_type in element_types
|
||||
if isinstance(element_type, FormElementType)
|
||||
if isinstance(element_type, FormElementTypeMixin)
|
||||
]
|
||||
other_element_types = [
|
||||
element_type
|
||||
for element_type in element_types
|
||||
if not isinstance(element_type, ContainerElementType)
|
||||
and not isinstance(element_type, FormElementType)
|
||||
if not isinstance(element_type, ContainerElementTypeMixin)
|
||||
and not isinstance(element_type, FormElementTypeMixin)
|
||||
]
|
||||
manual_ordering = container_element_types + form_element_types + other_element_types
|
||||
expected_ordering = sorted(
|
||||
|
|
|
@ -368,61 +368,121 @@ export class ElementType extends Registerable {
|
|||
}
|
||||
}
|
||||
|
||||
export class ContainerElementType extends ElementType {
|
||||
get elementTypesAll() {
|
||||
return Object.values(this.app.$registry.getAll('element'))
|
||||
}
|
||||
const ContainerElementTypeMixin = (Base) =>
|
||||
class extends Base {
|
||||
isContainerElementType = true
|
||||
|
||||
/**
|
||||
* Returns an array of element types that are not allowed as children of this element.
|
||||
*
|
||||
* @returns {Array}
|
||||
*/
|
||||
get childElementTypesForbidden() {
|
||||
return []
|
||||
}
|
||||
|
||||
get childElementTypes() {
|
||||
return _.difference(this.elementTypesAll, this.childElementTypesForbidden)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of style types that are not allowed as children of this element.
|
||||
* @returns {Array}
|
||||
*/
|
||||
get childStylesForbidden() {
|
||||
return []
|
||||
}
|
||||
|
||||
get defaultPlaceInContainer() {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default value when creating a child element to this container.
|
||||
* @param {Object} page The current page object
|
||||
* @param {Object} values The values of the to be created element
|
||||
* @returns the default values for this element.
|
||||
*/
|
||||
getDefaultChildValues(page, values) {
|
||||
// By default an element inside a container should have no left and right padding
|
||||
return { style_padding_left: 0, style_padding_right: 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a `page` and an `element`, move the child element of a container
|
||||
* in the direction specified by the `placement`.
|
||||
*
|
||||
* The default implementation only supports moving the element vertically.
|
||||
*
|
||||
* @param {Object} page The page that is the parent component.
|
||||
* @param {Number} element The child element to be moved.
|
||||
* @param {String} placement The direction in which the element should move.
|
||||
*/
|
||||
async moveChildElement(page, parentElement, element, placement) {
|
||||
if (placement === PLACEMENTS.AFTER || placement === PLACEMENTS.BEFORE) {
|
||||
await this.moveElementInSamePlace(page, element, placement)
|
||||
get elementTypesAll() {
|
||||
return Object.values(this.app.$registry.getAll('element'))
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of element types that are not allowed as children of this element.
|
||||
*
|
||||
* @returns {Array}
|
||||
*/
|
||||
get childElementTypesForbidden() {
|
||||
return []
|
||||
}
|
||||
|
||||
get childElementTypes() {
|
||||
return _.difference(this.elementTypesAll, this.childElementTypesForbidden)
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns an array of style types that are not allowed as children of this element.
|
||||
* @returns {Array}
|
||||
*/
|
||||
get childStylesForbidden() {
|
||||
return []
|
||||
}
|
||||
|
||||
get defaultPlaceInContainer() {
|
||||
throw new Error('Not implemented')
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns the default value when creating a child element to this container.
|
||||
* @param {Object} page The current page object
|
||||
* @param {Object} values The values of the to be created element
|
||||
* @returns the default values for this element.
|
||||
*/
|
||||
getDefaultChildValues(page, values) {
|
||||
// By default, an element inside a container should have no left and right padding
|
||||
return { style_padding_left: 0, style_padding_right: 0 }
|
||||
}
|
||||
|
||||
/**
|
||||
* Given a `page` and an `element`, move the child element of a container
|
||||
* in the direction specified by the `placement`.
|
||||
*
|
||||
* The default implementation only supports moving the element vertically.
|
||||
*
|
||||
* @param {Object} page The page that is the parent component.
|
||||
* @param {Number} element The child element to be moved.
|
||||
* @param {String} placement The direction in which the element should move.
|
||||
*/
|
||||
async moveChildElement(page, parentElement, element, placement) {
|
||||
if (placement === PLACEMENTS.AFTER || placement === PLACEMENTS.BEFORE) {
|
||||
await this.moveElementInSamePlace(page, element, placement)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of placements that are disallowed for the elements to move
|
||||
* in their container.
|
||||
*
|
||||
* @param {Object} page The page that is the parent component.
|
||||
* @param {Number} element The child element for which the placements should
|
||||
* be calculated.
|
||||
* @returns {Array} An array of placements that are disallowed for the element.
|
||||
*/
|
||||
getPlacementsDisabledForChild(page, containerElement, element) {
|
||||
this.getPlacementsDisabled(page, element)
|
||||
}
|
||||
|
||||
getNextHorizontalElementToSelect(page, element, placement) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
export class FormContainerElementType extends ContainerElementTypeMixin(
|
||||
ElementType
|
||||
) {
|
||||
static getType() {
|
||||
return 'form_container'
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.app.i18n.t('elementType.formContainer')
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this.app.i18n.t('elementType.formContainerDescription')
|
||||
}
|
||||
|
||||
get iconClass() {
|
||||
return 'iconoir-frame'
|
||||
}
|
||||
|
||||
get component() {
|
||||
return FormContainerElement
|
||||
}
|
||||
|
||||
get generalFormComponent() {
|
||||
return FormContainerElementForm
|
||||
}
|
||||
|
||||
get childElementTypesForbidden() {
|
||||
return this.elementTypesAll.filter((type) => !type.isFormElement)
|
||||
}
|
||||
|
||||
get events() {
|
||||
return [SubmitEvent]
|
||||
}
|
||||
|
||||
get childStylesForbidden() {
|
||||
return ['style_width']
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -435,15 +495,15 @@ export class ContainerElementType extends ElementType {
|
|||
* @returns {Array} An array of placements that are disallowed for the element.
|
||||
*/
|
||||
getPlacementsDisabledForChild(page, containerElement, element) {
|
||||
super.getPlacementsDisabled(page, element)
|
||||
}
|
||||
|
||||
getNextHorizontalElementToSelect(page, element, placement) {
|
||||
return null
|
||||
return [
|
||||
PLACEMENTS.LEFT,
|
||||
PLACEMENTS.RIGHT,
|
||||
...this.getVerticalPlacementsDisabled(page, element),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export class ColumnElementType extends ContainerElementType {
|
||||
export class ColumnElementType extends ContainerElementTypeMixin(ElementType) {
|
||||
getType() {
|
||||
return 'column'
|
||||
}
|
||||
|
@ -470,7 +530,7 @@ export class ColumnElementType extends ContainerElementType {
|
|||
|
||||
get childElementTypesForbidden() {
|
||||
return this.elementTypesAll.filter(
|
||||
(elementType) => elementType instanceof ContainerElementType
|
||||
(elementType) => elementType.isContainerElementType
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -566,8 +626,110 @@ export class ColumnElementType extends ContainerElementType {
|
|||
}
|
||||
}
|
||||
|
||||
const CollectionElementTypeMixin = (Base) => class extends Base {}
|
||||
|
||||
export class TableElementType extends CollectionElementTypeMixin(ElementType) {
|
||||
getType() {
|
||||
return 'table'
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.app.i18n.t('elementType.table')
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this.app.i18n.t('elementType.tableDescription')
|
||||
}
|
||||
|
||||
get iconClass() {
|
||||
return 'iconoir-table'
|
||||
}
|
||||
|
||||
get component() {
|
||||
return TableElement
|
||||
}
|
||||
|
||||
get generalFormComponent() {
|
||||
return TableElementForm
|
||||
}
|
||||
|
||||
async onElementEvent(event, { page, element, dataSourceId }) {
|
||||
if (event === ELEMENT_EVENTS.DATA_SOURCE_REMOVED) {
|
||||
if (element.data_source_id === dataSourceId) {
|
||||
// Remove the data_source_id
|
||||
await this.app.store.dispatch('element/forceUpdate', {
|
||||
page,
|
||||
element,
|
||||
values: { data_source_id: null },
|
||||
})
|
||||
// Empty the element content
|
||||
await this.app.store.dispatch('elementContent/clearElementContent', {
|
||||
element,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (event === ELEMENT_EVENTS.DATA_SOURCE_AFTER_UPDATE) {
|
||||
if (element.data_source_id === dataSourceId) {
|
||||
await this.app.store.dispatch(
|
||||
'elementContent/triggerElementContentReset',
|
||||
{
|
||||
element,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isInError({ element, builder }) {
|
||||
const collectionFieldsInError = element.fields.map((collectionField) => {
|
||||
const collectionFieldType = this.app.$registry.get(
|
||||
'collectionField',
|
||||
collectionField.type
|
||||
)
|
||||
return collectionFieldType.isInError({
|
||||
field: collectionField,
|
||||
builder,
|
||||
})
|
||||
})
|
||||
return collectionFieldsInError.includes(true)
|
||||
}
|
||||
|
||||
getDisplayName(element, { page }) {
|
||||
let suffix = ''
|
||||
|
||||
if (element.data_source_id) {
|
||||
const dataSource = this.app.store.getters[
|
||||
'dataSource/getPageDataSourceById'
|
||||
](page, element.data_source_id)
|
||||
|
||||
suffix = dataSource ? ` - ${dataSource.name}` : ''
|
||||
}
|
||||
|
||||
return `${this.name}${suffix}`
|
||||
}
|
||||
}
|
||||
|
||||
export class RepeatElementType extends ContainerElementTypeMixin(
|
||||
CollectionElementTypeMixin(ElementType)
|
||||
) {
|
||||
getType() {
|
||||
return 'repeat'
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.app.i18n.t('elementType.repeat')
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this.app.i18n.t('elementType.repeatDescription')
|
||||
}
|
||||
|
||||
get iconClass() {
|
||||
return 'iconoir-repeat'
|
||||
}
|
||||
}
|
||||
/**
|
||||
* This class servers as a parent class for all form element types. Form element types
|
||||
* This class serves as a parent class for all form element types. Form element types
|
||||
* are all elements that can be used as part of a form. So in simple terms, any element
|
||||
* that can represents data in a way that is directly modifiable by an application user.
|
||||
*/
|
||||
|
@ -918,87 +1080,6 @@ export class ButtonElementType extends ElementType {
|
|||
}
|
||||
}
|
||||
|
||||
export class TableElementType extends ElementType {
|
||||
getType() {
|
||||
return 'table'
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.app.i18n.t('elementType.table')
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this.app.i18n.t('elementType.tableDescription')
|
||||
}
|
||||
|
||||
get iconClass() {
|
||||
return 'iconoir-table'
|
||||
}
|
||||
|
||||
get component() {
|
||||
return TableElement
|
||||
}
|
||||
|
||||
get generalFormComponent() {
|
||||
return TableElementForm
|
||||
}
|
||||
|
||||
async onElementEvent(event, { page, element, dataSourceId }) {
|
||||
if (event === ELEMENT_EVENTS.DATA_SOURCE_REMOVED) {
|
||||
if (element.data_source_id === dataSourceId) {
|
||||
// Remove the data_source_id
|
||||
await this.app.store.dispatch('element/forceUpdate', {
|
||||
page,
|
||||
element,
|
||||
values: { data_source_id: null },
|
||||
})
|
||||
// Empty the element content
|
||||
await this.app.store.dispatch('elementContent/clearElementContent', {
|
||||
element,
|
||||
})
|
||||
}
|
||||
}
|
||||
if (event === ELEMENT_EVENTS.DATA_SOURCE_AFTER_UPDATE) {
|
||||
if (element.data_source_id === dataSourceId) {
|
||||
await this.app.store.dispatch(
|
||||
'elementContent/triggerElementContentReset',
|
||||
{
|
||||
element,
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
isInError({ element, builder }) {
|
||||
const collectionFieldsInError = element.fields.map((collectionField) => {
|
||||
const collectionFieldType = this.app.$registry.get(
|
||||
'collectionField',
|
||||
collectionField.type
|
||||
)
|
||||
return collectionFieldType.isInError({
|
||||
field: collectionField,
|
||||
builder,
|
||||
})
|
||||
})
|
||||
return collectionFieldsInError.includes(true)
|
||||
}
|
||||
|
||||
getDisplayName(element, { page }) {
|
||||
let suffix = ''
|
||||
|
||||
if (element.data_source_id) {
|
||||
const dataSource = this.app.store.getters[
|
||||
'dataSource/getPageDataSourceById'
|
||||
](page, element.data_source_id)
|
||||
|
||||
suffix = dataSource ? ` - ${dataSource.name}` : ''
|
||||
}
|
||||
|
||||
return `${this.name}${suffix}`
|
||||
}
|
||||
}
|
||||
|
||||
export class DropdownElementType extends FormElementType {
|
||||
static getType() {
|
||||
return 'dropdown'
|
||||
|
@ -1064,61 +1145,6 @@ export class DropdownElementType extends FormElementType {
|
|||
}
|
||||
}
|
||||
|
||||
export class FormContainerElementType extends ContainerElementType {
|
||||
static getType() {
|
||||
return 'form_container'
|
||||
}
|
||||
|
||||
get name() {
|
||||
return this.app.i18n.t('elementType.formContainer')
|
||||
}
|
||||
|
||||
get description() {
|
||||
return this.app.i18n.t('elementType.formContainerDescription')
|
||||
}
|
||||
|
||||
get iconClass() {
|
||||
return 'iconoir-frame'
|
||||
}
|
||||
|
||||
get component() {
|
||||
return FormContainerElement
|
||||
}
|
||||
|
||||
get generalFormComponent() {
|
||||
return FormContainerElementForm
|
||||
}
|
||||
|
||||
get childElementTypesForbidden() {
|
||||
return this.elementTypesAll.filter((type) => !type.isFormElement)
|
||||
}
|
||||
|
||||
get events() {
|
||||
return [SubmitEvent]
|
||||
}
|
||||
|
||||
get childStylesForbidden() {
|
||||
return ['style_width']
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of placements that are disallowed for the elements to move
|
||||
* in their container.
|
||||
*
|
||||
* @param {Object} page The page that is the parent component.
|
||||
* @param {Number} element The child element for which the placements should
|
||||
* be calculated.
|
||||
* @returns {Array} An array of placements that are disallowed for the element.
|
||||
*/
|
||||
getPlacementsDisabledForChild(page, containerElement, element) {
|
||||
return [
|
||||
PLACEMENTS.LEFT,
|
||||
PLACEMENTS.RIGHT,
|
||||
...this.getVerticalPlacementsDisabled(page, element),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
export class CheckboxElementType extends FormElementType {
|
||||
getType() {
|
||||
return 'checkbox'
|
||||
|
|
|
@ -93,7 +93,9 @@
|
|||
"iframe": "IFrame",
|
||||
"iframeDescription": "Inline frame",
|
||||
"authForm": "Login form",
|
||||
"authFormDescription": "A user login form"
|
||||
"authFormDescription": "A user login form",
|
||||
"repeat": "Repeat",
|
||||
"repeatDescription": "A repeatable set of elements"
|
||||
},
|
||||
"addElementButton": {
|
||||
"label": "Element"
|
||||
|
|
|
@ -38,6 +38,7 @@ import {
|
|||
DropdownElementType,
|
||||
CheckboxElementType,
|
||||
IFrameElementType,
|
||||
RepeatElementType,
|
||||
} from '@baserow/modules/builder/elementTypes'
|
||||
import {
|
||||
DesktopDeviceType,
|
||||
|
@ -171,6 +172,10 @@ export default (context) => {
|
|||
app.$registry.register('element', new DropdownElementType(context))
|
||||
app.$registry.register('element', new CheckboxElementType(context))
|
||||
|
||||
if (app.$featureFlagIsEnabled('builder-repeat-element')) {
|
||||
app.$registry.register('element', new RepeatElementType(context))
|
||||
}
|
||||
|
||||
app.$registry.register('device', new DesktopDeviceType(context))
|
||||
app.$registry.register('device', new TabletDeviceType(context))
|
||||
app.$registry.register('device', new SmartphoneDeviceType(context))
|
||||
|
|
Loading…
Add table
Reference in a new issue