1
0
Fork 0
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 

See merge request 
This commit is contained in:
Peter Evans 2024-04-15 09:14:31 +00:00
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

View file

@ -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."

View file

@ -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"]

View 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)

View file

@ -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(

View file

@ -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'

View file

@ -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"

View file

@ -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))