mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-03 04:35:31 +00:00
Merge branch '2583-repeat-element-add-current_record-data-provider-support' into 'develop'
Resolve "Repeat element: add current_record data provider support" Closes #2583 See merge request baserow/baserow!2355
This commit is contained in:
commit
4b38d3a49d
27 changed files with 664 additions and 81 deletions
backend
src/baserow
contrib/builder
api/workflow_actions
data_providers
data_sources
elements
core/services
test_utils/fixtures
tests/baserow/contrib/builder
data_providers
data_sources
elements
web-frontend
modules/builder
components
dataProviderTypes.jselementTypes.jsmixins
pages
store
test/unit/builder
|
@ -391,7 +391,9 @@ class DispatchBuilderWorkflowActionView(APIView):
|
|||
workflow_action_id
|
||||
)
|
||||
|
||||
dispatch_context = BuilderDispatchContext(request, workflow_action.page)
|
||||
dispatch_context = BuilderDispatchContext(
|
||||
request, workflow_action.page, workflow_action=workflow_action
|
||||
)
|
||||
|
||||
response = BuilderWorkflowActionService().dispatch_action(
|
||||
request.user, workflow_action, dispatch_context # type: ignore
|
||||
|
|
|
@ -13,7 +13,10 @@ from baserow.contrib.builder.data_sources.exceptions import (
|
|||
)
|
||||
from baserow.contrib.builder.data_sources.handler import DataSourceHandler
|
||||
from baserow.contrib.builder.elements.handler import ElementHandler
|
||||
from baserow.contrib.builder.elements.mixins import FormElementTypeMixin
|
||||
from baserow.contrib.builder.elements.mixins import (
|
||||
CollectionElementTypeMixin,
|
||||
FormElementTypeMixin,
|
||||
)
|
||||
from baserow.contrib.builder.elements.models import FormElement
|
||||
from baserow.contrib.builder.workflow_actions.handler import (
|
||||
BuilderWorkflowActionHandler,
|
||||
|
@ -185,9 +188,35 @@ class CurrentRecordDataProviderType(DataProviderType):
|
|||
type = "current_record"
|
||||
|
||||
def get_data_chunk(self, dispatch_context: BuilderDispatchContext, path: List[str]):
|
||||
"""Doesn't make sense in the backend yet"""
|
||||
"""
|
||||
Get the current record data from the request data.
|
||||
|
||||
return None
|
||||
:param dispatch_context: The dispatch context.
|
||||
:param path: The path to the data.
|
||||
:return: The data at the path.
|
||||
"""
|
||||
|
||||
try:
|
||||
current_record = dispatch_context.request.data["current_record"]
|
||||
except KeyError:
|
||||
return None
|
||||
|
||||
first_collection_element_ancestor = ElementHandler().get_first_ancestor_of_type(
|
||||
dispatch_context.workflow_action.element_id,
|
||||
CollectionElementTypeMixin,
|
||||
)
|
||||
data_source_id = first_collection_element_ancestor.specific.data_source_id
|
||||
|
||||
# Narrow down our range to just our record index.
|
||||
dispatch_context = BuilderDispatchContext.from_context(
|
||||
dispatch_context,
|
||||
offset=current_record,
|
||||
count=1,
|
||||
)
|
||||
|
||||
return DataSourceDataProviderType().get_data_chunk(
|
||||
dispatch_context, [data_source_id, "0", *path]
|
||||
)
|
||||
|
||||
def import_path(self, path, id_mapping, data_source_id=None, **kwargs):
|
||||
"""
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
from typing import TYPE_CHECKING, Optional
|
||||
|
||||
from rest_framework.request import Request
|
||||
|
||||
from baserow.contrib.builder.data_providers.registries import (
|
||||
|
@ -6,11 +8,28 @@ from baserow.contrib.builder.data_providers.registries import (
|
|||
from baserow.contrib.builder.pages.models import Page
|
||||
from baserow.core.services.dispatch_context import DispatchContext
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from baserow.core.workflow_actions.models import WorkflowAction
|
||||
|
||||
|
||||
class BuilderDispatchContext(DispatchContext):
|
||||
def __init__(self, request: Request, page: Page):
|
||||
own_properties = ["request", "page", "workflow_action", "offset", "count"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
request: Request,
|
||||
page: Page,
|
||||
workflow_action: Optional["WorkflowAction"] = None,
|
||||
offset: Optional[int] = None,
|
||||
count: Optional[int] = None,
|
||||
):
|
||||
self.request = request
|
||||
self.page = page
|
||||
self.workflow_action = workflow_action
|
||||
|
||||
# Overrides the `request` GET offset/count values.
|
||||
self.offset = offset
|
||||
self.count = count
|
||||
|
||||
super().__init__()
|
||||
|
||||
|
@ -19,17 +38,24 @@ class BuilderDispatchContext(DispatchContext):
|
|||
return builder_data_provider_type_registry
|
||||
|
||||
def range(self, service):
|
||||
"""Return page range from the GET parameters."""
|
||||
"""
|
||||
Return page range from the `offset`, `count` kwargs,
|
||||
or the GET parameters.
|
||||
"""
|
||||
|
||||
try:
|
||||
offset = int(self.request.GET.get("offset", 0))
|
||||
except ValueError:
|
||||
offset = 0
|
||||
if self.offset is not None and self.count is not None:
|
||||
offset = self.offset
|
||||
count = self.count
|
||||
else:
|
||||
try:
|
||||
offset = int(self.request.GET.get("offset", 0))
|
||||
except ValueError:
|
||||
offset = 0
|
||||
|
||||
try:
|
||||
count = int(self.request.GET.get("count", 20))
|
||||
except ValueError:
|
||||
count = 20
|
||||
try:
|
||||
count = int(self.request.GET.get("count", 20))
|
||||
except ValueError:
|
||||
count = 20
|
||||
|
||||
# max prevent negative values
|
||||
return [
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
from collections import defaultdict
|
||||
from typing import Any, Dict, Iterable, List, Optional, Union, cast
|
||||
from typing import Any, Dict, Iterable, List, Optional, Type, Union, cast
|
||||
from zipfile import ZipFile
|
||||
|
||||
from django.core.files.storage import Storage
|
||||
|
@ -12,17 +12,22 @@ from baserow.contrib.builder.elements.exceptions import (
|
|||
from baserow.contrib.builder.elements.models import ContainerElement, Element
|
||||
from baserow.contrib.builder.elements.registries import (
|
||||
ElementType,
|
||||
ElementTypeSubClass,
|
||||
element_type_registry,
|
||||
)
|
||||
from baserow.contrib.builder.elements.types import (
|
||||
ElementForUpdate,
|
||||
ElementsAndWorkflowActions,
|
||||
)
|
||||
from baserow.contrib.builder.pages.models import Page
|
||||
from baserow.contrib.builder.workflow_actions.models import BuilderWorkflowAction
|
||||
from baserow.contrib.builder.workflow_actions.registries import (
|
||||
builder_workflow_action_type_registry,
|
||||
)
|
||||
from baserow.core.db import specific_iterator
|
||||
from baserow.core.exceptions import IdDoesNotExist
|
||||
from baserow.core.utils import MirrorDict, extract_allowed
|
||||
|
||||
from ..workflow_actions.models import BuilderWorkflowAction
|
||||
from ..workflow_actions.registries import builder_workflow_action_type_registry
|
||||
from .types import ElementForUpdate, ElementsAndWorkflowActions
|
||||
|
||||
|
||||
class ElementHandler:
|
||||
allowed_fields_create = [
|
||||
|
@ -94,6 +99,47 @@ class ElementHandler:
|
|||
|
||||
return element
|
||||
|
||||
def get_ancestors(self, element_id: int, page: Page) -> List[Element]:
|
||||
"""
|
||||
Returns a list of all the ancestors of the given element. The ancestry
|
||||
results are cached in the dispatch context to avoid multiple queries in
|
||||
the same HTTP request.
|
||||
|
||||
:param element_id: The ID of the element.
|
||||
:param page: The page that holds the elements.
|
||||
:return: A list of the ancestors of the given element.
|
||||
"""
|
||||
|
||||
elements = self.get_elements(page)
|
||||
grouped_elements = {element.id: element for element in elements}
|
||||
element = grouped_elements[element_id]
|
||||
|
||||
ancestry = []
|
||||
while element.parent_element_id is not None:
|
||||
element = grouped_elements[element.parent_element_id]
|
||||
ancestry.append(element)
|
||||
|
||||
return ancestry
|
||||
|
||||
def get_first_ancestor_of_type(
|
||||
self,
|
||||
element_id: int,
|
||||
target_type: Type[ElementTypeSubClass],
|
||||
) -> Optional[Element]:
|
||||
"""
|
||||
Returns the first ancestor of the given type belonging to this element.
|
||||
|
||||
:param element_id: The ID of the element.
|
||||
:param target_type: The type of the element to find.
|
||||
:return: The first ancestor of the given type or None if not found.
|
||||
"""
|
||||
|
||||
element = ElementHandler().get_element(element_id)
|
||||
for ancestor in self.get_ancestors(element_id, element.page):
|
||||
ancestor_type = element_type_registry.get_by_model(ancestor.specific_class)
|
||||
if isinstance(ancestor_type, target_type):
|
||||
return ancestor
|
||||
|
||||
def get_element_for_update(
|
||||
self, element_id: int, base_queryset: Optional[QuerySet] = None
|
||||
) -> ElementForUpdate:
|
||||
|
@ -120,6 +166,7 @@ class ElementHandler:
|
|||
page: Page,
|
||||
base_queryset: Optional[QuerySet] = None,
|
||||
specific: bool = True,
|
||||
use_cache: bool = True,
|
||||
) -> Union[QuerySet[Element], Iterable[Element]]:
|
||||
"""
|
||||
Gets all the specific elements of a given page.
|
||||
|
@ -128,18 +175,38 @@ class ElementHandler:
|
|||
:param base_queryset: The base queryset to use to build the query.
|
||||
:param specific: Whether to return the generic elements or the specific
|
||||
instances.
|
||||
:param use_cache: Whether to use the cached elements on the page or not.
|
||||
:return: The elements of that page.
|
||||
"""
|
||||
|
||||
# Our cache key varies if we're using specific or generic elements.
|
||||
cache_key = "_page_elements" if not specific else "_page_elements_specific"
|
||||
|
||||
# If a `base_queryset` is given, we can't use caching, clear
|
||||
# both cache keys in case the specific argument has changed.
|
||||
if base_queryset is not None:
|
||||
use_cache = False
|
||||
setattr(page, "_page_elements", None)
|
||||
setattr(page, "_page_elements_specific", None)
|
||||
|
||||
elements_cache = getattr(page, cache_key, None)
|
||||
if use_cache and elements_cache is not None:
|
||||
return elements_cache
|
||||
|
||||
queryset = base_queryset if base_queryset is not None else Element.objects.all()
|
||||
|
||||
queryset = queryset.filter(page=page)
|
||||
|
||||
if specific:
|
||||
queryset = queryset.select_related("content_type")
|
||||
return specific_iterator(queryset)
|
||||
elements = specific_iterator(queryset)
|
||||
else:
|
||||
return queryset
|
||||
elements = queryset
|
||||
|
||||
if use_cache:
|
||||
setattr(page, cache_key, list(elements))
|
||||
|
||||
return elements
|
||||
|
||||
def create_element(
|
||||
self,
|
||||
|
|
|
@ -2,9 +2,12 @@ from abc import ABC, abstractmethod
|
|||
|
||||
from baserow.core.formula.runtime_formula_context import RuntimeFormulaContext
|
||||
from baserow.core.services.models import Service
|
||||
from baserow.core.services.types import RuntimeFormulaContextSubClass
|
||||
|
||||
|
||||
class DispatchContext(RuntimeFormulaContext, ABC):
|
||||
own_properties = []
|
||||
|
||||
def __init__(self):
|
||||
self.cache = {} # can be used by data providers to save queries
|
||||
super().__init__()
|
||||
|
@ -16,3 +19,24 @@ class DispatchContext(RuntimeFormulaContext, ABC):
|
|||
|
||||
:params service: The service we want the pagination for.
|
||||
"""
|
||||
|
||||
@classmethod
|
||||
def from_context(
|
||||
cls, context: RuntimeFormulaContextSubClass, **kwargs
|
||||
) -> RuntimeFormulaContextSubClass:
|
||||
"""
|
||||
Return a new DispatchContext instance from the given context, without
|
||||
losing the original cached data.
|
||||
|
||||
:params context: The context to create a new DispatchContext instance from.
|
||||
"""
|
||||
|
||||
new_values = {}
|
||||
for prop in cls.own_properties:
|
||||
new_values[prop] = getattr(context, prop)
|
||||
new_values.update(kwargs)
|
||||
|
||||
new_context = cls(**new_values)
|
||||
new_context.cache = {**context.cache}
|
||||
|
||||
return new_context
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from typing import NewType, Optional, TypedDict, TypeVar
|
||||
|
||||
from .models import Service
|
||||
from baserow.core.formula.runtime_formula_context import RuntimeFormulaContext
|
||||
from baserow.core.services.models import Service
|
||||
|
||||
|
||||
class ServiceDict(TypedDict):
|
||||
|
@ -34,3 +35,7 @@ ServiceSortDictSubClass = TypeVar("ServiceSortDictSubClass", bound="ServiceSortD
|
|||
ServiceSubClass = TypeVar("ServiceSubClass", bound="Service")
|
||||
|
||||
ServiceForUpdate = NewType("ServiceForUpdate", Service)
|
||||
|
||||
RuntimeFormulaContextSubClass = TypeVar(
|
||||
"RuntimeFormulaContextSubClass", bound=RuntimeFormulaContext
|
||||
)
|
||||
|
|
|
@ -35,10 +35,10 @@ class DataSourceFixtures:
|
|||
|
||||
service = kwargs.pop("service", None)
|
||||
if service is None and service_model_class:
|
||||
integrations_args = kwargs.pop("integration_args", {})
|
||||
integrations_args["application"] = page.builder
|
||||
service = self.create_service(
|
||||
service_model_class,
|
||||
integration_args={"application": page.builder},
|
||||
**kwargs,
|
||||
service_model_class, integration_args=integrations_args, **kwargs
|
||||
)
|
||||
|
||||
if order is None:
|
||||
|
|
|
@ -10,6 +10,7 @@ from baserow.contrib.builder.elements.models import (
|
|||
ImageElement,
|
||||
InputTextElement,
|
||||
LinkElement,
|
||||
RepeatElement,
|
||||
TableElement,
|
||||
TextElement,
|
||||
)
|
||||
|
@ -96,6 +97,14 @@ class ElementFixtures:
|
|||
element = self.create_builder_element(DropdownElement, user, page, **kwargs)
|
||||
return element
|
||||
|
||||
def create_builder_repeat_element(self, user=None, page=None, **kwargs):
|
||||
if "data_source" not in kwargs:
|
||||
kwargs[
|
||||
"data_source"
|
||||
] = self.create_builder_local_baserow_list_rows_data_source(page=page)
|
||||
element = self.create_builder_element(RepeatElement, user, page, **kwargs)
|
||||
return element
|
||||
|
||||
def create_builder_element(self, model_class, user=None, page=None, **kwargs):
|
||||
if user is None:
|
||||
user = self.create_user()
|
||||
|
|
|
@ -22,6 +22,7 @@ from baserow.contrib.builder.data_sources.builder_dispatch_context import (
|
|||
)
|
||||
from baserow.contrib.builder.elements.handler import ElementHandler
|
||||
from baserow.contrib.builder.formula_importer import import_formula
|
||||
from baserow.contrib.builder.workflow_actions.models import EventTypes
|
||||
from baserow.core.services.dispatch_context import DispatchContext
|
||||
from baserow.core.services.exceptions import ServiceImproperlyConfigured
|
||||
from baserow.core.user_sources.user_source_user import UserSourceUser
|
||||
|
@ -759,6 +760,72 @@ def test_user_data_provider_get_data_chunk(data_fixture):
|
|||
assert user_data_provider_type.get_data_chunk(dispatch_context, ["id"]) == 42
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_current_record_provider_get_data_chunk_without_record_index(data_fixture):
|
||||
current_record_provider = CurrentRecordDataProviderType()
|
||||
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
|
||||
fake_request = MagicMock()
|
||||
fake_request.data = {}
|
||||
|
||||
builder = data_fixture.create_builder_application(user=user)
|
||||
page = data_fixture.create_builder_page(user=user, builder=builder)
|
||||
workflow_action = data_fixture.create_local_baserow_create_row_workflow_action(
|
||||
page=page, event=EventTypes.CLICK, user=user
|
||||
)
|
||||
|
||||
dispatch_context = BuilderDispatchContext(fake_request, page, workflow_action)
|
||||
assert current_record_provider.get_data_chunk(dispatch_context, ["path"]) is None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_current_record_provider_get_data_chunk(data_fixture):
|
||||
current_record_provider = CurrentRecordDataProviderType()
|
||||
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
|
||||
fake_request = MagicMock()
|
||||
fake_request.user = user
|
||||
fake_request.data = {"current_record": 0}
|
||||
|
||||
table, fields, rows = data_fixture.build_table(
|
||||
user=user,
|
||||
columns=[
|
||||
("Animal", "text"),
|
||||
],
|
||||
rows=[
|
||||
["Badger"],
|
||||
["Horse"],
|
||||
["Bison"],
|
||||
],
|
||||
)
|
||||
field = table.field_set.get(name="Animal")
|
||||
builder = data_fixture.create_builder_application(user=user)
|
||||
page = data_fixture.create_builder_page(user=user, builder=builder)
|
||||
|
||||
data_source = data_fixture.create_builder_local_baserow_list_rows_data_source(
|
||||
page=page, table=table, integration_args={"authorized_user": user}
|
||||
)
|
||||
repeat_element = data_fixture.create_builder_repeat_element(
|
||||
page=page, data_source=data_source
|
||||
)
|
||||
button_element = data_fixture.create_builder_button_element(
|
||||
page=page, parent_element=repeat_element
|
||||
)
|
||||
|
||||
workflow_action = data_fixture.create_local_baserow_create_row_workflow_action(
|
||||
page=page, element=button_element, event=EventTypes.CLICK, user=user
|
||||
)
|
||||
|
||||
dispatch_context = BuilderDispatchContext(fake_request, page, workflow_action)
|
||||
|
||||
assert (
|
||||
current_record_provider.get_data_chunk(dispatch_context, [field.db_column])
|
||||
== "Badger"
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_current_record_provider_type_import_path(data_fixture):
|
||||
# When a `current_record` provider is imported, and the path only contains the
|
||||
|
|
|
@ -1,5 +1,10 @@
|
|||
from unittest.mock import MagicMock
|
||||
|
||||
from django.http import HttpRequest
|
||||
|
||||
import pytest
|
||||
from rest_framework.request import Request
|
||||
|
||||
from baserow.contrib.builder.data_sources.builder_dispatch_context import (
|
||||
BuilderDispatchContext,
|
||||
)
|
||||
|
@ -24,3 +29,24 @@ def test_dispatch_context_page_range():
|
|||
dispatch_context = BuilderDispatchContext(request, None)
|
||||
|
||||
assert dispatch_context.range(None) == [0, 1]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_dispatch_context_page_from_context(data_fixture):
|
||||
user = data_fixture.create_user()
|
||||
page = data_fixture.create_builder_page(user=user)
|
||||
request = Request(HttpRequest())
|
||||
request.user = user
|
||||
|
||||
dispatch_context = BuilderDispatchContext(request, page, offset=0, count=5)
|
||||
dispatch_context.annotated_data = "foobar"
|
||||
dispatch_context.cache = {"key": "value"}
|
||||
new_dispatch_context = BuilderDispatchContext.from_context(
|
||||
dispatch_context, offset=5, count=1
|
||||
)
|
||||
assert getattr(new_dispatch_context, "annotated_data", None) is None
|
||||
assert new_dispatch_context.cache == {"key": "value"}
|
||||
assert new_dispatch_context.request == request
|
||||
assert new_dispatch_context.page == page
|
||||
assert new_dispatch_context.offset == 5
|
||||
assert new_dispatch_context.count == 1
|
||||
|
|
|
@ -54,13 +54,17 @@ def test_get_element_does_not_exist(data_fixture):
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_elements(data_fixture):
|
||||
def test_get_elements(data_fixture, django_assert_num_queries):
|
||||
page = data_fixture.create_builder_page()
|
||||
element1 = data_fixture.create_builder_heading_element(page=page)
|
||||
element2 = data_fixture.create_builder_heading_element(page=page)
|
||||
element3 = data_fixture.create_builder_text_element(page=page)
|
||||
|
||||
elements = ElementHandler().get_elements(page)
|
||||
with django_assert_num_queries(3):
|
||||
elements = ElementHandler().get_elements(page)
|
||||
|
||||
# Cache of specific elements is set.
|
||||
assert getattr(page, "_page_elements_specific") == elements
|
||||
|
||||
assert [e.id for e in elements] == [
|
||||
element1.id,
|
||||
|
@ -69,8 +73,31 @@ def test_get_elements(data_fixture):
|
|||
]
|
||||
|
||||
assert isinstance(elements[0], HeadingElement)
|
||||
assert isinstance(elements[1], HeadingElement)
|
||||
assert isinstance(elements[2], TextElement)
|
||||
|
||||
# Cache of specific elements is re-used.
|
||||
with django_assert_num_queries(0):
|
||||
elements = ElementHandler().get_elements(page)
|
||||
assert getattr(page, "_page_elements_specific") == elements
|
||||
|
||||
# We request non-specific records, the cache changes.
|
||||
with django_assert_num_queries(1):
|
||||
elements = list(ElementHandler().get_elements(page, specific=False))
|
||||
assert getattr(page, "_page_elements") == elements
|
||||
|
||||
# We request non-specific records, the cache is reused.
|
||||
with django_assert_num_queries(0):
|
||||
elements = list(ElementHandler().get_elements(page, specific=False))
|
||||
assert getattr(page, "_page_elements") == elements
|
||||
|
||||
# We pass in a base queryset, no caching strategy is available.
|
||||
base_queryset = Element.objects.filter(page=page, visibility="all")
|
||||
with django_assert_num_queries(3):
|
||||
ElementHandler().get_elements(page, base_queryset)
|
||||
assert getattr(page, "_page_elements") is None
|
||||
assert getattr(page, "_page_elements_specific") is None
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_delete_element(data_fixture):
|
||||
|
@ -505,3 +532,44 @@ def test_duplicate_element_with_workflow_action_in_container(data_fixture):
|
|||
]
|
||||
assert duplicated_workflow_action1.page_id == workflow_action1.page_id
|
||||
assert duplicated_workflow_action2.page_id == workflow_action2.page_id
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_ancestors(data_fixture, django_assert_num_queries):
|
||||
page = data_fixture.create_builder_page()
|
||||
grandparent = data_fixture.create_builder_column_element(column_amount=1, page=page)
|
||||
parent = data_fixture.create_builder_column_element(
|
||||
column_amount=3, parent_element=grandparent, page=page
|
||||
)
|
||||
child = data_fixture.create_builder_heading_element(
|
||||
page=page, parent_element=parent
|
||||
)
|
||||
|
||||
# Query and cache the page's elements for the same context.
|
||||
# Query 1: fetch the elements on the page.
|
||||
# 2: fetch the specific column types.
|
||||
# 3: fetch the specific heading type.
|
||||
with django_assert_num_queries(3):
|
||||
ancestors = ElementHandler().get_ancestors(child.id, page)
|
||||
|
||||
assert len(ancestors) == 2
|
||||
assert ancestors == [parent, grandparent]
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_get_first_ancestor_of_type(data_fixture, django_assert_num_queries):
|
||||
page = data_fixture.create_builder_page()
|
||||
grandparent = data_fixture.create_builder_column_element(column_amount=1, page=page)
|
||||
parent = data_fixture.create_builder_form_container_element(
|
||||
parent_element=grandparent, page=page
|
||||
)
|
||||
child = data_fixture.create_builder_dropdown_element(
|
||||
page=page, parent_element=parent
|
||||
)
|
||||
|
||||
with django_assert_num_queries(7):
|
||||
nearest_column_ancestor = ElementHandler().get_first_ancestor_of_type(
|
||||
child.id, ColumnElementType
|
||||
)
|
||||
|
||||
assert nearest_column_ancestor.specific == grandparent
|
||||
|
|
|
@ -22,6 +22,7 @@
|
|||
<script>
|
||||
import FormulaInputGroup from '@baserow/modules/core/components/formula/FormulaInputGroup'
|
||||
import { DataSourceDataProviderType } from '@baserow/modules/builder/dataProviderTypes'
|
||||
|
||||
export default {
|
||||
name: 'ApplicationBuilderFormulaInputGroup',
|
||||
components: { FormulaInputGroup },
|
||||
|
|
|
@ -34,7 +34,12 @@
|
|||
@select-parent="selectParentElement()"
|
||||
/>
|
||||
|
||||
<PageElement :element="element" :mode="mode" class="element--read-only" />
|
||||
<PageElement
|
||||
:element="element"
|
||||
:mode="mode"
|
||||
class="element--read-only"
|
||||
:application-context-additions="applicationContextAdditions"
|
||||
/>
|
||||
|
||||
<InsertElementButton
|
||||
v-show="isSelected"
|
||||
|
@ -95,6 +100,11 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
applicationContextAdditions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -144,7 +154,10 @@ export default {
|
|||
return elementType.getPlacementsDisabled(this.page, this.element)
|
||||
},
|
||||
elementTypesAllowed() {
|
||||
return this.parentElementType?.childElementTypes || null
|
||||
return (
|
||||
this.parentElementType?.childElementTypes(this.page, this.element) ||
|
||||
null
|
||||
)
|
||||
},
|
||||
canCreate() {
|
||||
return this.$hasPermission(
|
||||
|
|
|
@ -21,12 +21,14 @@
|
|||
<ElementPreview
|
||||
v-if="mode === 'editing'"
|
||||
:element="childCurrent"
|
||||
:application-context-additions="applicationContextAdditions"
|
||||
@move="move(childCurrent, $event)"
|
||||
></ElementPreview>
|
||||
<PageElement
|
||||
v-else
|
||||
:element="childCurrent"
|
||||
:mode="mode"
|
||||
:application-context-additions="applicationContextAdditions"
|
||||
></PageElement>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -41,7 +43,7 @@
|
|||
<AddElementModal
|
||||
ref="addElementModal"
|
||||
:page="page"
|
||||
:element-types-allowed="elementType.childElementTypes"
|
||||
:element-types-allowed="elementType.childElementTypes(page, element)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -79,6 +81,11 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
applicationContextAdditions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
flexAlignment() {
|
||||
|
|
|
@ -15,7 +15,7 @@
|
|||
<AddElementModal
|
||||
ref="addElementModal"
|
||||
:page="page"
|
||||
:element-types-allowed="elementType.childElementTypes"
|
||||
:element-types-allowed="elementType.childElementTypes(page, element)"
|
||||
></AddElementModal>
|
||||
</div>
|
||||
<div v-else>
|
||||
|
|
|
@ -13,6 +13,9 @@
|
|||
v-if="index === 0 && isEditMode"
|
||||
:key="child.id"
|
||||
:element="child"
|
||||
:application-context-additions="{
|
||||
recordIndex: index,
|
||||
}"
|
||||
@move="moveElement(child, $event)"
|
||||
/>
|
||||
<!-- Other iterations are not editable -->
|
||||
|
@ -22,6 +25,9 @@
|
|||
:key="child.id"
|
||||
:element="child"
|
||||
:force-mode="'preview'"
|
||||
:application-context-additions="{
|
||||
recordIndex: index,
|
||||
}"
|
||||
:class="{
|
||||
'repeat-element-preview': index > 0 && isEditMode,
|
||||
}"
|
||||
|
@ -36,7 +42,7 @@
|
|||
<AddElementModal
|
||||
ref="addElementModal"
|
||||
:page="page"
|
||||
:element-types-allowed="elementType.childElementTypes"
|
||||
:element-types-allowed="elementType.childElementTypes(page, element)"
|
||||
></AddElementModal>
|
||||
</template>
|
||||
</template>
|
||||
|
@ -48,7 +54,7 @@
|
|||
<AddElementModal
|
||||
ref="addElementModal"
|
||||
:page="page"
|
||||
:element-types-allowed="elementType.childElementTypes"
|
||||
:element-types-allowed="elementType.childElementTypes(page, element)"
|
||||
></AddElementModal>
|
||||
</template>
|
||||
<!-- We have no contents, but we do have children in edit mode -->
|
||||
|
|
|
@ -32,10 +32,15 @@ import { BACKGROUND_TYPES, WIDTH_TYPES } from '@baserow/modules/builder/enums'
|
|||
|
||||
export default {
|
||||
name: 'PageElement',
|
||||
inject: ['builder', 'page', 'mode'],
|
||||
inject: ['builder', 'page', 'mode', 'applicationContext'],
|
||||
provide() {
|
||||
return {
|
||||
mode: this.elementMode,
|
||||
applicationContext: {
|
||||
...this.applicationContext,
|
||||
element: this.element,
|
||||
...this.applicationContextAdditions,
|
||||
},
|
||||
}
|
||||
},
|
||||
props: {
|
||||
|
@ -48,6 +53,11 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
applicationContextAdditions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
BACKGROUND_TYPES: () => BACKGROUND_TYPES,
|
||||
|
|
|
@ -103,6 +103,7 @@ export default {
|
|||
actionUpdateDataSource: 'dataSource/debouncedUpdate',
|
||||
actionDeleteDataSource: 'dataSource/delete',
|
||||
actionFetchDataSources: 'dataSource/fetch',
|
||||
clearElementContent: 'elementContent/clearElementContent',
|
||||
}),
|
||||
async shown() {
|
||||
try {
|
||||
|
|
|
@ -15,5 +15,14 @@ import elementSidePanel from '@baserow/modules/builder/mixins/elementSidePanel'
|
|||
export default {
|
||||
name: 'GeneralSidePanel',
|
||||
mixins: [elementSidePanel],
|
||||
inject: ['applicationContext'],
|
||||
provide() {
|
||||
return {
|
||||
applicationContext: {
|
||||
...this.applicationContext,
|
||||
element: this.element,
|
||||
},
|
||||
}
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -213,6 +213,26 @@ export class CurrentRecordDataProviderType extends DataProviderType {
|
|||
return true
|
||||
}
|
||||
|
||||
getFirstCollectionAncestor(page, element) {
|
||||
if (!element) {
|
||||
return null
|
||||
}
|
||||
const elementType = this.app.$registry.get('element', element.type)
|
||||
if (elementType.isCollectionElement) {
|
||||
return element
|
||||
}
|
||||
const ancestors = this.app.store.getters['element/getAncestors'](
|
||||
page,
|
||||
element
|
||||
)
|
||||
for (const ancestor of ancestors) {
|
||||
const ancestorType = this.app.$registry.get('element', ancestor.type)
|
||||
if (ancestorType.isCollectionElement) {
|
||||
return ancestor
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Loads all element contents
|
||||
async init(applicationContext) {
|
||||
const { page } = applicationContext
|
||||
|
@ -252,20 +272,26 @@ export class CurrentRecordDataProviderType extends DataProviderType {
|
|||
)
|
||||
}
|
||||
|
||||
getActionDispatchContext(applicationContext) {
|
||||
return applicationContext.recordIndex
|
||||
}
|
||||
|
||||
getDataChunk(applicationContext, path) {
|
||||
const content = this.getDataContent(applicationContext)
|
||||
return getValueAtPath(content, path.join('.'))
|
||||
}
|
||||
|
||||
getDataContent(applicationContext) {
|
||||
const { element, recordIndex = 0 } = applicationContext
|
||||
|
||||
if (!element) {
|
||||
const { page, element, recordIndex = 0 } = applicationContext
|
||||
const collectionElement = this.getFirstCollectionAncestor(page, element)
|
||||
if (!collectionElement) {
|
||||
return []
|
||||
}
|
||||
|
||||
const rows =
|
||||
this.app.store.getters['elementContent/getElementContent'](element)
|
||||
this.app.store.getters['elementContent/getElementContent'](
|
||||
collectionElement
|
||||
)
|
||||
|
||||
const row = { [this.indexKey]: recordIndex, ...rows[recordIndex] }
|
||||
|
||||
|
@ -284,8 +310,9 @@ export class CurrentRecordDataProviderType extends DataProviderType {
|
|||
}
|
||||
|
||||
getDataSchema(applicationContext) {
|
||||
const { page, element: { data_source_id: dataSourceId } = {} } =
|
||||
applicationContext
|
||||
const { page, element } = applicationContext
|
||||
const collectionElement = this.getFirstCollectionAncestor(page, element)
|
||||
const dataSourceId = collectionElement?.data_source_id
|
||||
|
||||
if (!dataSourceId) {
|
||||
return null
|
||||
|
@ -312,8 +339,9 @@ export class CurrentRecordDataProviderType extends DataProviderType {
|
|||
|
||||
getPathTitle(applicationContext, pathParts) {
|
||||
if (pathParts.length === 1) {
|
||||
const { page, element: { data_source_id: dataSourceId } = {} } =
|
||||
applicationContext
|
||||
const { page, element } = applicationContext
|
||||
const collectionElement = this.getFirstCollectionAncestor(page, element)
|
||||
const dataSourceId = collectionElement?.data_source_id
|
||||
|
||||
const dataSource = this.app.store.getters[
|
||||
'dataSource/getPageDataSourceById'
|
||||
|
|
|
@ -372,22 +372,44 @@ export class ElementType extends Registerable {
|
|||
|
||||
const ContainerElementTypeMixin = (Base) =>
|
||||
class extends Base {
|
||||
isContainerElementType = true
|
||||
isContainerElement = true
|
||||
|
||||
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}
|
||||
* Returns an array of element types that are not allowed as children of this element type.
|
||||
* @returns {Array} An array of forbidden child element types.
|
||||
*/
|
||||
get childElementTypesForbidden() {
|
||||
return []
|
||||
}
|
||||
|
||||
get childElementTypes() {
|
||||
/**
|
||||
* Returns an array of element types that are allowed as children of this element.
|
||||
* If the parent element we're trying to add a child to has a parent, we'll check
|
||||
* each parent until the root element if they have any forbidden element types to
|
||||
* include as well.
|
||||
* @param page
|
||||
* @param element
|
||||
* @returns {Array} An array of permitted child element types.
|
||||
*/
|
||||
childElementTypes(page, element) {
|
||||
if (element.parent_element_id) {
|
||||
const parentElement = this.app.store.getters['element/getElementById'](
|
||||
page,
|
||||
element.parent_element_id
|
||||
)
|
||||
const parentElementType = this.app.$registry.get(
|
||||
'element',
|
||||
parentElement.type
|
||||
)
|
||||
return _.difference(
|
||||
parentElementType.childElementTypes(page, parentElement),
|
||||
this.childElementTypesForbidden
|
||||
)
|
||||
}
|
||||
return _.difference(this.elementTypesAll, this.childElementTypesForbidden)
|
||||
}
|
||||
|
||||
|
@ -475,6 +497,10 @@ export class FormContainerElementType extends ContainerElementTypeMixin(
|
|||
return FormContainerElementForm
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude element types which are not a form element.
|
||||
* @returns {Array} An array of non-form element types.
|
||||
*/
|
||||
get childElementTypesForbidden() {
|
||||
return this.elementTypesAll.filter((type) => !type.isFormElement)
|
||||
}
|
||||
|
@ -530,9 +556,13 @@ export class ColumnElementType extends ContainerElementTypeMixin(ElementType) {
|
|||
return ColumnElementForm
|
||||
}
|
||||
|
||||
/**
|
||||
* Exclude element types which are containers.
|
||||
* @returns {Array} An array of container element types.
|
||||
*/
|
||||
get childElementTypesForbidden() {
|
||||
return this.elementTypesAll.filter(
|
||||
(elementType) => elementType.isContainerElementType
|
||||
(elementType) => elementType.isContainerElement
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -630,6 +660,7 @@ export class ColumnElementType extends ContainerElementTypeMixin(ElementType) {
|
|||
|
||||
const CollectionElementTypeMixin = (Base) =>
|
||||
class extends Base {
|
||||
isCollectionElement = true
|
||||
getDisplayName(element, { page }) {
|
||||
let suffix = ''
|
||||
|
||||
|
@ -715,7 +746,7 @@ export class TableElementType extends CollectionElementTypeMixin(ElementType) {
|
|||
export class RepeatElementType extends ContainerElementTypeMixin(
|
||||
CollectionElementTypeMixin(ElementType)
|
||||
) {
|
||||
getType() {
|
||||
static getType() {
|
||||
return 'repeat'
|
||||
}
|
||||
|
||||
|
@ -739,6 +770,20 @@ export class RepeatElementType extends ContainerElementTypeMixin(
|
|||
return RepeatElementForm
|
||||
}
|
||||
|
||||
/**
|
||||
* The repeat elements will disallow itself, all form elements, and the
|
||||
* form container, from being added as children.
|
||||
* @returns {Array} An array of disallowed child element types.
|
||||
*/
|
||||
get childElementTypesForbidden() {
|
||||
const repeatElement = this.app.$registry.get('element', 'repeat')
|
||||
const formContainer = this.app.$registry.get('element', 'form_container')
|
||||
return this.elementTypesAll.filter(
|
||||
(type) =>
|
||||
type.isFormElement || type === formContainer || type === repeatElement
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Return an array of placements that are disallowed for the elements to move
|
||||
* in their container.
|
||||
|
|
|
@ -9,12 +9,26 @@ import { resolveColor } from '@baserow/modules/core/utils/colors'
|
|||
import { themeToColorVariables } from '@baserow/modules/builder/utils/theme'
|
||||
|
||||
export default {
|
||||
inject: ['workspace', 'builder', 'page', 'mode'],
|
||||
inject: ['workspace', 'builder', 'page', 'mode', 'applicationContext'],
|
||||
provide() {
|
||||
return {
|
||||
applicationContext: {
|
||||
...this.applicationContext,
|
||||
element: this.element,
|
||||
...this.applicationContextAdditions,
|
||||
},
|
||||
}
|
||||
},
|
||||
props: {
|
||||
element: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
applicationContextAdditions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
workflowActionsInProgress() {
|
||||
|
@ -31,14 +45,6 @@ export default {
|
|||
isEditMode() {
|
||||
return this.mode === 'editing'
|
||||
},
|
||||
applicationContext() {
|
||||
return {
|
||||
builder: this.builder,
|
||||
page: this.page,
|
||||
mode: this.mode,
|
||||
element: this.element,
|
||||
}
|
||||
},
|
||||
runtimeFormulaContext() {
|
||||
/**
|
||||
* This proxy allow the RuntimeFormulaContextClass to act like a regular object.
|
||||
|
|
|
@ -37,6 +37,7 @@ export default {
|
|||
page: this.page,
|
||||
mode,
|
||||
formulaComponent: ApplicationBuilderFormulaInputGroup,
|
||||
applicationContext: this.applicationContext,
|
||||
}
|
||||
},
|
||||
/**
|
||||
|
|
|
@ -34,6 +34,7 @@ export default {
|
|||
page: this.page,
|
||||
mode: this.mode,
|
||||
formulaComponent: ApplicationBuilderFormulaInputGroup,
|
||||
applicationContext: this.applicationContext,
|
||||
}
|
||||
},
|
||||
async asyncData({ store, params, error, $registry, app, req, route }) {
|
||||
|
|
|
@ -254,6 +254,20 @@ const actions = {
|
|||
})
|
||||
throw error
|
||||
}
|
||||
// After deleting the data source, find all collection elements
|
||||
// which use this data source, and clear their element content.
|
||||
const dataSourceCollectionElements = page.elements.filter((element) => {
|
||||
return element.data_source_id === dataSourceToDelete.id
|
||||
})
|
||||
dataSourceCollectionElements.map(async (collectionElement) => {
|
||||
await dispatch(
|
||||
'elementContent/clearElementContent',
|
||||
{
|
||||
element: collectionElement,
|
||||
},
|
||||
{ root: true }
|
||||
)
|
||||
})
|
||||
commit('SET_LOADING', { page, value: false })
|
||||
},
|
||||
async fetch({ dispatch, commit }, { page }) {
|
||||
|
|
|
@ -60,7 +60,7 @@ const orderElements = (elements, parentElementId = null) => {
|
|||
const updateCachedValues = (page) => {
|
||||
page.orderedElements = orderElements(page.elements)
|
||||
page.elementMap = Object.fromEntries(
|
||||
page.orderedElements.map((element) => [`${element.id}`, element])
|
||||
page.elements.map((element) => [`${element.id}`, element])
|
||||
)
|
||||
}
|
||||
|
||||
|
@ -120,11 +120,7 @@ const actions = {
|
|||
elementType.afterUpdate(element, page)
|
||||
},
|
||||
forceDelete({ commit, getters }, { page, elementId }) {
|
||||
const elementsOfPage = getters.getElementsOrdered(page)
|
||||
const elementIndex = elementsOfPage.findIndex(
|
||||
(element) => element.id === elementId
|
||||
)
|
||||
const elementToDelete = elementsOfPage[elementIndex]
|
||||
const elementToDelete = getters.getElementById(page, elementId)
|
||||
|
||||
if (getters.getSelected?.id === elementId) {
|
||||
commit('SELECT_ITEM', { element: null })
|
||||
|
@ -276,12 +272,7 @@ const actions = {
|
|||
})
|
||||
},
|
||||
async delete({ dispatch, getters }, { page, elementId }) {
|
||||
const elementsOfPage = getters.getElementsOrdered(page)
|
||||
const elementIndex = elementsOfPage.findIndex(
|
||||
(element) => element.id === elementId
|
||||
)
|
||||
const elementToDelete = elementsOfPage[elementIndex]
|
||||
|
||||
const elementToDelete = getters.getElementById(page, elementId)
|
||||
const descendants = getters.getDescendants(page, elementToDelete)
|
||||
|
||||
// First delete all children
|
||||
|
@ -456,17 +447,25 @@ const getters = {
|
|||
getParent: (state, getters) => (page, element) => {
|
||||
return getters.getElementById(page, element?.parent_element_id)
|
||||
},
|
||||
getAncestors: (state, getters) => (page, element) => {
|
||||
const getElementAncestors = (element) => {
|
||||
const parentElement = getters.getParent(page, element)
|
||||
if (parentElement) {
|
||||
return [...getElementAncestors(parentElement), parentElement]
|
||||
} else {
|
||||
return []
|
||||
/**
|
||||
* Given an element, return all its ancestors until we reach the root element.
|
||||
* If `parentFirst` is `true` then we reverse the array of elements so that
|
||||
* the element's immediate parent is first, otherwise the root element will be first.
|
||||
*/
|
||||
getAncestors:
|
||||
(state, getters) =>
|
||||
(page, element, parentFirst = true) => {
|
||||
const getElementAncestors = (element) => {
|
||||
const parentElement = getters.getParent(page, element)
|
||||
if (parentElement) {
|
||||
return [...getElementAncestors(parentElement), parentElement]
|
||||
} else {
|
||||
return []
|
||||
}
|
||||
}
|
||||
}
|
||||
return getElementAncestors(element)
|
||||
},
|
||||
const ancestors = getElementAncestors(element)
|
||||
return parentFirst ? ancestors.reverse() : ancestors
|
||||
},
|
||||
getSiblings: (state, getters) => (page, element) => {
|
||||
return getters
|
||||
.getElementsOrdered(page)
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import {
|
||||
ElementType,
|
||||
CheckboxElementType,
|
||||
DropdownElementType,
|
||||
ElementType,
|
||||
InputTextElementType,
|
||||
} from '@baserow/modules/builder/elementTypes'
|
||||
import { TestApp } from '@baserow/test/helpers/testApp'
|
||||
import _ from 'lodash'
|
||||
|
||||
describe('elementTypes tests', () => {
|
||||
const testApp = new TestApp()
|
||||
|
@ -346,4 +347,122 @@ describe('elementTypes tests', () => {
|
|||
expect(elementType.isValid(element, 'uk')).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('elementType childElementTypesForbidden tests', () => {
|
||||
test('FormContainerElementType forbids non-form elements as children.', () => {
|
||||
const formContainerElementType = testApp
|
||||
.getRegistry()
|
||||
.get('element', 'form_container')
|
||||
const nonFormElementTypes = Object.values(
|
||||
testApp.getRegistry().getAll('element')
|
||||
)
|
||||
.filter((elementType) => !elementType.isFormElement)
|
||||
.map((elementType) => elementType.getType())
|
||||
|
||||
const forbiddenChildTypes =
|
||||
formContainerElementType.childElementTypesForbidden.map((el) =>
|
||||
el.getType()
|
||||
)
|
||||
expect(forbiddenChildTypes).toEqual(nonFormElementTypes)
|
||||
})
|
||||
test('ColumnElementType forbids container elements as children.', () => {
|
||||
const columnElementType = testApp.getRegistry().get('element', 'column')
|
||||
const containerElementTypes = Object.values(
|
||||
testApp.getRegistry().getAll('element')
|
||||
)
|
||||
.filter((elementType) => elementType.isContainerElement)
|
||||
.map((elementType) => elementType.getType())
|
||||
|
||||
const forbiddenChildTypes =
|
||||
columnElementType.childElementTypesForbidden.map((el) => el.getType())
|
||||
expect(forbiddenChildTypes).toEqual(containerElementTypes)
|
||||
})
|
||||
test('RepeatElementType forbids itself, form elements and the form container as children.', () => {
|
||||
const repeatElementType = testApp.getRegistry().get('element', 'repeat')
|
||||
|
||||
const formContainerElementType = testApp
|
||||
.getRegistry()
|
||||
.get('element', 'form_container')
|
||||
|
||||
let expectedForbiddenChildTypes = [
|
||||
repeatElementType.type,
|
||||
formContainerElementType.type,
|
||||
]
|
||||
const formElementTypes = Object.values(
|
||||
testApp.getRegistry().getAll('element')
|
||||
)
|
||||
.filter((elementType) => elementType.isFormElement)
|
||||
.map((elementType) => elementType.getType())
|
||||
|
||||
expectedForbiddenChildTypes =
|
||||
expectedForbiddenChildTypes.concat(formElementTypes)
|
||||
|
||||
const forbiddenChildTypes =
|
||||
repeatElementType.childElementTypesForbidden.map((el) => el.getType())
|
||||
expect(forbiddenChildTypes.sort()).toEqual(
|
||||
expectedForbiddenChildTypes.sort()
|
||||
)
|
||||
})
|
||||
})
|
||||
|
||||
describe('elementType childElementTypes tests', () => {
|
||||
test('childElementTypes called with element with no parent only restricts child types to its own requirements.', () => {
|
||||
const page = { id: 1, name: 'Contact Us' }
|
||||
const element = { id: 2, parent_element_id: null }
|
||||
const formContainerElementType = testApp
|
||||
.getRegistry()
|
||||
.get('element', 'form_container')
|
||||
const formElementTypes = Object.values(
|
||||
testApp.getRegistry().getAll('element')
|
||||
)
|
||||
.filter((elementType) => elementType.isFormElement)
|
||||
.map((elementType) => elementType.getType())
|
||||
|
||||
const childElementTypes = formContainerElementType
|
||||
.childElementTypes(page, element)
|
||||
.map((el) => el.getType())
|
||||
expect(childElementTypes).toEqual(formElementTypes)
|
||||
})
|
||||
test('childElementTypes called with element with parent restricts child types using all ancestor childElementTypes requirements.', () => {
|
||||
const page = { id: 1, name: 'Contact Us' }
|
||||
const parentElement = {
|
||||
id: 1,
|
||||
page_id: page.id,
|
||||
parent_element_id: null,
|
||||
type: 'repeat',
|
||||
}
|
||||
const element = {
|
||||
id: 2,
|
||||
page_id: page.id,
|
||||
parent_element_id: parentElement.id,
|
||||
type: 'column',
|
||||
}
|
||||
page.elementMap = { 1: parentElement, 2: element }
|
||||
|
||||
const allElementTypes = Object.values(
|
||||
testApp.getRegistry().getAll('element')
|
||||
).map((el) => el.getType())
|
||||
|
||||
const columnElementType = testApp.getRegistry().get('element', 'column')
|
||||
const forbiddenColumnChildTypes =
|
||||
columnElementType.childElementTypesForbidden.map((el) => el.getType())
|
||||
|
||||
const repeatElementType = testApp.getRegistry().get('element', 'repeat')
|
||||
const forbiddenRepeatChildTypes =
|
||||
repeatElementType.childElementTypesForbidden.map((el) => el.getType())
|
||||
|
||||
const allExpectedForbiddenChildTypes = forbiddenColumnChildTypes.concat(
|
||||
forbiddenRepeatChildTypes
|
||||
)
|
||||
const expectedAllowedChildTypes = _.difference(
|
||||
allElementTypes,
|
||||
allExpectedForbiddenChildTypes
|
||||
)
|
||||
|
||||
const childElementTypes = columnElementType
|
||||
.childElementTypes(page, element)
|
||||
.map((el) => el.getType())
|
||||
expect(childElementTypes).toEqual(expectedAllowedChildTypes)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
Loading…
Add table
Reference in a new issue