mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-14 00:59:06 +00:00
Resolve "Create a multi-page container element"
This commit is contained in:
parent
05a33a182a
commit
b10a65666a
124 changed files with 3474 additions and 1729 deletions
backend
src/baserow/contrib/builder
tests/baserow/contrib
builder
api
data_sources
domains
elements
test_element_handler.pytest_element_service.pytest_element_types.pytest_header_footer_element_type.pytest_record_selector_element_type.py
pages
test_builder_application_type.pytest_formula_property_extractor.pytest_permissions_manager.pyintegrations/local_baserow
changelog/entries/unreleased/feature
e2e-tests/tests/builder
enterprise
backend/tests/baserow_enterprise_tests/integrations/local_baserow
web-frontend/modules/baserow_enterprise/builder/components/elements
web-frontend/modules/builder
applicationTypes.js
components
ApplicationBuilderFormulaInput.vue
elementTypeMixins.jselementTypes.jsenums.jsdataSource
elements
AddElementCard.vueAddElementModal.vueAddElementZone.vueElementMenu.vueElementPreview.vueElementsList.vueElementsListItem.vue
baseComponents
components
event
page
CreatePageModal.vuePageContent.vuePageElement.vuePagePreview.vuePageTemplate.vuePageTemplateContent.vueUserSourceUsersContext.vue
header
settings
sidePanels
settings
workflowAction
locales
mixins
collectionElement.jscollectionElementForm.jscollectionField.jscontainerElement.jselement.jselementForm.jselementSidePanel.jsformElement.jsformElementForm.jsvisibilityForm.js
pages
|
@ -254,7 +254,9 @@ class DataSourceView(APIView):
|
||||||
if "page_id" in request.data:
|
if "page_id" in request.data:
|
||||||
page = PageHandler().get_page(
|
page = PageHandler().get_page(
|
||||||
int(request.data["page_id"]),
|
int(request.data["page_id"]),
|
||||||
base_queryset=Page.objects.filter(builder=data_source.page.builder),
|
base_queryset=Page.objects_with_shared.filter(
|
||||||
|
builder=data_source.page.builder
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Do we have a service?
|
# Do we have a service?
|
||||||
|
|
|
@ -24,6 +24,7 @@ from baserow.contrib.builder.domains.registries import domain_type_registry
|
||||||
from baserow.contrib.builder.elements.models import Element
|
from baserow.contrib.builder.elements.models import Element
|
||||||
from baserow.contrib.builder.elements.registries import element_type_registry
|
from baserow.contrib.builder.elements.registries import element_type_registry
|
||||||
from baserow.contrib.builder.models import Builder
|
from baserow.contrib.builder.models import Builder
|
||||||
|
from baserow.contrib.builder.pages.handler import PageHandler
|
||||||
from baserow.contrib.builder.pages.models import Page
|
from baserow.contrib.builder.pages.models import Page
|
||||||
from baserow.core.services.registries import service_type_registry
|
from baserow.core.services.registries import service_type_registry
|
||||||
from baserow.core.user_sources.models import UserSource
|
from baserow.core.user_sources.models import UserSource
|
||||||
|
@ -112,6 +113,7 @@ class PublicElementSerializer(serializers.ModelSerializer):
|
||||||
"page_id",
|
"page_id",
|
||||||
"type",
|
"type",
|
||||||
"order",
|
"order",
|
||||||
|
"page_id",
|
||||||
"parent_element_id",
|
"parent_element_id",
|
||||||
"place_in_container",
|
"place_in_container",
|
||||||
"visibility",
|
"visibility",
|
||||||
|
@ -272,7 +274,7 @@ class PublicBuilderSerializer(serializers.ModelSerializer):
|
||||||
:return: A list of serialized pages that belong to this instance.
|
:return: A list of serialized pages that belong to this instance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pages = instance.page_set.all()
|
pages = PageHandler().get_pages(instance)
|
||||||
|
|
||||||
return PublicPageSerializer(pages, many=True).data
|
return PublicPageSerializer(pages, many=True).data
|
||||||
|
|
||||||
|
|
|
@ -133,6 +133,7 @@ class ElementsView(APIView):
|
||||||
@map_exceptions(
|
@map_exceptions(
|
||||||
{
|
{
|
||||||
PageDoesNotExist: ERROR_PAGE_DOES_NOT_EXIST,
|
PageDoesNotExist: ERROR_PAGE_DOES_NOT_EXIST,
|
||||||
|
ElementNotInSamePage: ERROR_ELEMENT_NOT_IN_SAME_PAGE,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@validate_body_custom_fields(
|
@validate_body_custom_fields(
|
||||||
|
|
|
@ -48,7 +48,7 @@ class BuilderSerializer(serializers.ModelSerializer):
|
||||||
:return: A list of serialized pages that belong to this instance.
|
:return: A list of serialized pages that belong to this instance.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
pages = instance.page_set.all()
|
pages = PageHandler().get_pages(instance)
|
||||||
|
|
||||||
user = self.context.get("user")
|
user = self.context.get("user")
|
||||||
request = self.context.get("request")
|
request = self.context.get("request")
|
||||||
|
|
|
@ -167,7 +167,12 @@ class BuilderApplicationType(ApplicationType):
|
||||||
for us in UserSourceHandler().get_user_sources(builder)
|
for us in UserSourceHandler().get_user_sources(builder)
|
||||||
]
|
]
|
||||||
|
|
||||||
pages = builder.page_set.all().prefetch_related("element_set", "datasource_set")
|
pages = PageHandler().get_pages(
|
||||||
|
builder,
|
||||||
|
base_queryset=Page.objects_with_shared.prefetch_related(
|
||||||
|
"element_set", "datasource_set"
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
serialized_pages = [
|
serialized_pages = [
|
||||||
PageHandler().export_page(
|
PageHandler().export_page(
|
||||||
|
|
|
@ -175,7 +175,9 @@ class BuilderConfig(AppConfig):
|
||||||
ChoiceElementType,
|
ChoiceElementType,
|
||||||
ColumnElementType,
|
ColumnElementType,
|
||||||
DateTimePickerElementType,
|
DateTimePickerElementType,
|
||||||
|
FooterElementType,
|
||||||
FormContainerElementType,
|
FormContainerElementType,
|
||||||
|
HeaderElementType,
|
||||||
HeadingElementType,
|
HeadingElementType,
|
||||||
IFrameElementType,
|
IFrameElementType,
|
||||||
ImageElementType,
|
ImageElementType,
|
||||||
|
@ -203,6 +205,8 @@ class BuilderConfig(AppConfig):
|
||||||
element_type_registry.register(CheckboxElementType())
|
element_type_registry.register(CheckboxElementType())
|
||||||
element_type_registry.register(IFrameElementType())
|
element_type_registry.register(IFrameElementType())
|
||||||
element_type_registry.register(DateTimePickerElementType())
|
element_type_registry.register(DateTimePickerElementType())
|
||||||
|
element_type_registry.register(HeaderElementType())
|
||||||
|
element_type_registry.register(FooterElementType())
|
||||||
|
|
||||||
from .domains.domain_types import CustomDomainType, SubDomainType
|
from .domains.domain_types import CustomDomainType, SubDomainType
|
||||||
from .domains.registries import domain_type_registry
|
from .domains.registries import domain_type_registry
|
||||||
|
|
|
@ -33,6 +33,7 @@ from baserow.contrib.builder.elements.mixins import (
|
||||||
CollectionElementWithFieldsTypeMixin,
|
CollectionElementWithFieldsTypeMixin,
|
||||||
ContainerElementTypeMixin,
|
ContainerElementTypeMixin,
|
||||||
FormElementTypeMixin,
|
FormElementTypeMixin,
|
||||||
|
MultiPageElementTypeMixin,
|
||||||
)
|
)
|
||||||
from baserow.contrib.builder.elements.models import (
|
from baserow.contrib.builder.elements.models import (
|
||||||
INPUT_TEXT_TYPES,
|
INPUT_TEXT_TYPES,
|
||||||
|
@ -43,7 +44,9 @@ from baserow.contrib.builder.elements.models import (
|
||||||
ColumnElement,
|
ColumnElement,
|
||||||
DateTimePickerElement,
|
DateTimePickerElement,
|
||||||
Element,
|
Element,
|
||||||
|
FooterElement,
|
||||||
FormContainerElement,
|
FormContainerElement,
|
||||||
|
HeaderElement,
|
||||||
HeadingElement,
|
HeadingElement,
|
||||||
IFrameElement,
|
IFrameElement,
|
||||||
ImageElement,
|
ImageElement,
|
||||||
|
@ -117,7 +120,7 @@ class ColumnElementType(ContainerElementTypeMixin, ElementType):
|
||||||
type = "column"
|
type = "column"
|
||||||
model_class = ColumnElement
|
model_class = ColumnElement
|
||||||
|
|
||||||
class SerializedDict(ElementDict):
|
class SerializedDict(ContainerElementTypeMixin.SerializedDict):
|
||||||
column_amount: int
|
column_amount: int
|
||||||
column_gap: int
|
column_gap: int
|
||||||
alignment: str
|
alignment: str
|
||||||
|
@ -191,8 +194,8 @@ class ColumnElementType(ContainerElementTypeMixin, ElementType):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return [
|
return [
|
||||||
element_type.type
|
element_type
|
||||||
for element_type in element_type_registry.get_all()
|
for element_type in super().child_types_allowed
|
||||||
if element_type.type != self.type
|
if element_type.type != self.type
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -210,7 +213,7 @@ class FormContainerElementType(ContainerElementTypeMixin, ElementType):
|
||||||
]
|
]
|
||||||
simple_formula_fields = ["submit_button_label"]
|
simple_formula_fields = ["submit_button_label"]
|
||||||
|
|
||||||
class SerializedDict(ElementDict):
|
class SerializedDict(ContainerElementTypeMixin.SerializedDict):
|
||||||
submit_button_label: BaserowFormula
|
submit_button_label: BaserowFormula
|
||||||
reset_initial_values_post_submission: bool
|
reset_initial_values_post_submission: bool
|
||||||
|
|
||||||
|
@ -261,8 +264,8 @@ class FormContainerElementType(ContainerElementTypeMixin, ElementType):
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return [
|
return [
|
||||||
element_type.type
|
element_type
|
||||||
for element_type in element_type_registry.get_all()
|
for element_type in super().child_types_allowed
|
||||||
if element_type.type != self.type
|
if element_type.type != self.type
|
||||||
]
|
]
|
||||||
|
|
||||||
|
@ -858,6 +861,16 @@ class NavigationElementManager:
|
||||||
"target": "blank",
|
"target": "blank",
|
||||||
}
|
}
|
||||||
|
|
||||||
|
def validate_place(
|
||||||
|
self,
|
||||||
|
page: Page,
|
||||||
|
parent_element: Optional[Element],
|
||||||
|
place_in_container: str,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
We need it because it's called in the prepare_value_for_db.
|
||||||
|
"""
|
||||||
|
|
||||||
def prepare_value_for_db(
|
def prepare_value_for_db(
|
||||||
self, values: Dict, instance: Optional[LinkElement] = None
|
self, values: Dict, instance: Optional[LinkElement] = None
|
||||||
):
|
):
|
||||||
|
@ -1939,3 +1952,35 @@ class DateTimePickerElementType(FormElementTypeMixin, ElementType):
|
||||||
"include_time": False,
|
"include_time": False,
|
||||||
"time_format": DATE_TIME_FORMAT_CHOICES[0][0],
|
"time_format": DATE_TIME_FORMAT_CHOICES[0][0],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class MultiPageContainerElementType(
|
||||||
|
ContainerElementTypeMixin, MultiPageElementTypeMixin, ElementType
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
A base class container element that can be displayed on multiple pages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class SerializedDict(
|
||||||
|
MultiPageElementTypeMixin.SerializedDict,
|
||||||
|
ContainerElementTypeMixin.SerializedDict,
|
||||||
|
):
|
||||||
|
...
|
||||||
|
|
||||||
|
|
||||||
|
class HeaderElementType(MultiPageContainerElementType):
|
||||||
|
"""
|
||||||
|
A container element that can be displayed on multiple pages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
type = "header"
|
||||||
|
model_class = HeaderElement
|
||||||
|
|
||||||
|
|
||||||
|
class FooterElementType(MultiPageContainerElementType):
|
||||||
|
"""
|
||||||
|
A container element that can be displayed on multiple pages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
type = "footer"
|
||||||
|
model_class = FooterElement
|
||||||
|
|
|
@ -40,6 +40,7 @@ from baserow.contrib.builder.elements.types import (
|
||||||
ElementSubClass,
|
ElementSubClass,
|
||||||
)
|
)
|
||||||
from baserow.contrib.builder.formula_importer import import_formula
|
from baserow.contrib.builder.formula_importer import import_formula
|
||||||
|
from baserow.contrib.builder.pages.handler import PageHandler
|
||||||
from baserow.contrib.builder.types import ElementDict
|
from baserow.contrib.builder.types import ElementDict
|
||||||
from baserow.contrib.database.fields.utils import get_field_id_from_field_key
|
from baserow.contrib.database.fields.utils import get_field_id_from_field_key
|
||||||
from baserow.core.formula.types import BaserowFormula
|
from baserow.core.formula.types import BaserowFormula
|
||||||
|
@ -59,10 +60,14 @@ class ContainerElementTypeMixin:
|
||||||
"""
|
"""
|
||||||
Lets you define which children types can be placed inside the container.
|
Lets you define which children types can be placed inside the container.
|
||||||
|
|
||||||
:return: All the allowed children types
|
By default, multi-page elements are not allowed inside any container.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
return [element_type.type for element_type in element_type_registry.get_all()]
|
return [
|
||||||
|
element_type
|
||||||
|
for element_type in element_type_registry.get_all()
|
||||||
|
if not element_type.is_multi_page_element
|
||||||
|
]
|
||||||
|
|
||||||
def get_new_place_in_container(
|
def get_new_place_in_container(
|
||||||
self, container_element: ContainerElement, places_removed: List[str]
|
self, container_element: ContainerElement, places_removed: List[str]
|
||||||
|
@ -128,6 +133,8 @@ class ContainerElementTypeMixin:
|
||||||
:raises DRFValidationError: If the place in container is invalid
|
:raises DRFValidationError: If the place in container is invalid
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
return True
|
||||||
|
|
||||||
|
|
||||||
class CollectionElementTypeMixin:
|
class CollectionElementTypeMixin:
|
||||||
is_collection_element = True
|
is_collection_element = True
|
||||||
|
@ -738,3 +745,119 @@ class FormElementTypeMixin:
|
||||||
)
|
)
|
||||||
|
|
||||||
return value
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
class MultiPageElementTypeMixin:
|
||||||
|
is_multi_page_element = True
|
||||||
|
|
||||||
|
@property
|
||||||
|
def serializer_field_names(self):
|
||||||
|
return super().serializer_field_names + [
|
||||||
|
"share_type",
|
||||||
|
"pages",
|
||||||
|
]
|
||||||
|
|
||||||
|
@property
|
||||||
|
def allowed_fields(self):
|
||||||
|
return super().allowed_fields + [
|
||||||
|
"share_type",
|
||||||
|
]
|
||||||
|
|
||||||
|
class SerializedDict(ElementDict):
|
||||||
|
share_type: str
|
||||||
|
pages: List[int]
|
||||||
|
|
||||||
|
def after_create(self, instance, values):
|
||||||
|
"""
|
||||||
|
Add the pages
|
||||||
|
"""
|
||||||
|
|
||||||
|
from baserow.contrib.builder.pages.models import Page
|
||||||
|
|
||||||
|
super().after_create(instance, values)
|
||||||
|
|
||||||
|
if "pages" in values:
|
||||||
|
pages = PageHandler().get_pages(
|
||||||
|
instance.page.builder,
|
||||||
|
base_queryset=Page.objects.filter(
|
||||||
|
id__in=[p.id for p in values["pages"]]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
instance.pages.add(*pages)
|
||||||
|
|
||||||
|
def after_update(self, instance: Any, values: Dict, changes: Dict[str, Tuple]):
|
||||||
|
"""
|
||||||
|
Updates the pages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
from baserow.contrib.builder.pages.models import Page
|
||||||
|
|
||||||
|
super().after_update(instance, values, changes)
|
||||||
|
|
||||||
|
if "pages" in values:
|
||||||
|
pages = PageHandler().get_pages(
|
||||||
|
instance.page.builder,
|
||||||
|
base_queryset=Page.objects.filter(
|
||||||
|
id__in=[p.id for p in values["pages"]]
|
||||||
|
),
|
||||||
|
)
|
||||||
|
instance.pages.clear()
|
||||||
|
instance.pages.add(*pages)
|
||||||
|
|
||||||
|
def serialize_property(
|
||||||
|
self,
|
||||||
|
element: "MultiPageElementTypeMixin",
|
||||||
|
prop_name: str,
|
||||||
|
files_zip=None,
|
||||||
|
storage=None,
|
||||||
|
cache=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
You can customize the behavior of the serialization of a property with this
|
||||||
|
hook.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if prop_name == "pages":
|
||||||
|
return [page.id for page in element.pages.all()]
|
||||||
|
|
||||||
|
return super().serialize_property(
|
||||||
|
element,
|
||||||
|
prop_name,
|
||||||
|
files_zip=files_zip,
|
||||||
|
storage=storage,
|
||||||
|
cache=cache,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
def create_instance_from_serialized(
|
||||||
|
self,
|
||||||
|
serialized_values: Dict[str, Any],
|
||||||
|
id_mapping,
|
||||||
|
files_zip=None,
|
||||||
|
storage=None,
|
||||||
|
cache=None,
|
||||||
|
**kwargs,
|
||||||
|
):
|
||||||
|
"""Deals with the fields"""
|
||||||
|
|
||||||
|
pages = serialized_values.pop("pages", [])
|
||||||
|
|
||||||
|
instance = super().create_instance_from_serialized(
|
||||||
|
serialized_values,
|
||||||
|
id_mapping,
|
||||||
|
files_zip=files_zip,
|
||||||
|
storage=storage,
|
||||||
|
cache=cache,
|
||||||
|
**kwargs,
|
||||||
|
)
|
||||||
|
|
||||||
|
pages = [id_mapping["builder_pages"][page_id] for page_id in pages]
|
||||||
|
|
||||||
|
if pages:
|
||||||
|
instance.pages.add(*pages)
|
||||||
|
|
||||||
|
return instance
|
||||||
|
|
||||||
|
def get_pytest_params(self, pytest_data_fixture) -> Dict[str, Any]:
|
||||||
|
return {"share_type": "all"}
|
||||||
|
|
|
@ -906,3 +906,38 @@ class DateTimePickerElement(FormElement):
|
||||||
max_length=32,
|
max_length=32,
|
||||||
help_text="24 (14:00) or 12 (02:30) PM",
|
help_text="24 (14:00) or 12 (02:30) PM",
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MultiPageElement(Element):
|
||||||
|
"""
|
||||||
|
A container element that can contain other elements and be can shared across
|
||||||
|
multiple pages.
|
||||||
|
"""
|
||||||
|
|
||||||
|
class SHARE_TYPE(models.TextChoices):
|
||||||
|
ALL = "all"
|
||||||
|
ONLY = "only"
|
||||||
|
EXCEPT = "except"
|
||||||
|
|
||||||
|
share_type = models.CharField(
|
||||||
|
choices=SHARE_TYPE.choices,
|
||||||
|
max_length=10,
|
||||||
|
default=SHARE_TYPE.ALL,
|
||||||
|
)
|
||||||
|
|
||||||
|
pages = models.ManyToManyField("builder.Page", blank=True)
|
||||||
|
|
||||||
|
class Meta:
|
||||||
|
abstract = True
|
||||||
|
|
||||||
|
|
||||||
|
class HeaderElement(MultiPageElement, ContainerElement):
|
||||||
|
"""
|
||||||
|
A multi-page container element positioned at the top of the page.
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
class FooterElement(MultiPageElement, ContainerElement):
|
||||||
|
"""
|
||||||
|
A multi-page container element positioned at the bottom of the page.
|
||||||
|
"""
|
||||||
|
|
|
@ -21,6 +21,7 @@ from rest_framework.exceptions import ValidationError
|
||||||
|
|
||||||
from baserow.contrib.builder.formula_importer import import_formula
|
from baserow.contrib.builder.formula_importer import import_formula
|
||||||
from baserow.contrib.builder.mixins import BuilderInstanceWithFormulaMixin
|
from baserow.contrib.builder.mixins import BuilderInstanceWithFormulaMixin
|
||||||
|
from baserow.contrib.builder.pages.models import Page
|
||||||
from baserow.contrib.database.db.functions import RandomUUID
|
from baserow.contrib.database.db.functions import RandomUUID
|
||||||
from baserow.core.registry import (
|
from baserow.core.registry import (
|
||||||
CustomFieldsInstanceMixin,
|
CustomFieldsInstanceMixin,
|
||||||
|
@ -58,6 +59,9 @@ class ElementType(
|
||||||
parent_property_name = "page"
|
parent_property_name = "page"
|
||||||
id_mapping_name = BUILDER_PAGE_ELEMENTS
|
id_mapping_name = BUILDER_PAGE_ELEMENTS
|
||||||
|
|
||||||
|
# Whether this element is a multi-page element and should be placed on shared page.
|
||||||
|
is_multi_page_element = False
|
||||||
|
|
||||||
# The order in which this element type is imported in `import_elements`.
|
# The order in which this element type is imported in `import_elements`.
|
||||||
# By default, the priority is `0`, the lowest value. If this property is
|
# By default, the priority is `0`, the lowest value. If this property is
|
||||||
# not overridden, then the instance is imported last.
|
# not overridden, then the instance is imported last.
|
||||||
|
@ -80,25 +84,62 @@ class ElementType(
|
||||||
parent_element_id = values.get(
|
parent_element_id = values.get(
|
||||||
"parent_element_id", getattr(instance, "parent_element_id", None)
|
"parent_element_id", getattr(instance, "parent_element_id", None)
|
||||||
)
|
)
|
||||||
place_in_container = values.get("place_in_container", None)
|
|
||||||
|
|
||||||
|
if instance:
|
||||||
|
place_in_container = values.get(
|
||||||
|
"place_in_container", instance.place_in_container
|
||||||
|
)
|
||||||
|
page = values.get("page", instance.page)
|
||||||
|
else:
|
||||||
|
place_in_container = values.get("place_in_container", None)
|
||||||
|
page = values["page"]
|
||||||
|
|
||||||
|
parent_element = None
|
||||||
if parent_element_id is not None:
|
if parent_element_id is not None:
|
||||||
parent_element = ElementHandler().get_element(parent_element_id)
|
parent_element = ElementHandler().get_element(parent_element_id)
|
||||||
parent_element_type = element_type_registry.get_by_model(parent_element)
|
|
||||||
|
|
||||||
if self.type not in parent_element_type.child_types_allowed:
|
# Validate the place for this element
|
||||||
|
self.validate_place(page, parent_element, place_in_container)
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
def validate_place(
|
||||||
|
self,
|
||||||
|
page: Page,
|
||||||
|
parent_element: Optional[ElementSubClass],
|
||||||
|
place_in_container: str,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Validates the page/parent_element/place_in_container for this element.
|
||||||
|
Can be overridden to change the behaviour.
|
||||||
|
|
||||||
|
:param page: the page we want to add/move the element to.
|
||||||
|
:param parent_element: the parent_element if any.
|
||||||
|
:param place_in_container: the place in container in the parent.
|
||||||
|
:raises ValidationError: if the the element place is not allowed.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if parent_element:
|
||||||
|
if self.type not in [
|
||||||
|
e.type for e in parent_element.get_type().child_types_allowed
|
||||||
|
]:
|
||||||
raise ValidationError(
|
raise ValidationError(
|
||||||
f"Container of type {parent_element_type.type} can't have child of "
|
f"Container of type {parent_element.get_type().type} can't have child of "
|
||||||
f"type {self.type}"
|
f"type {self.type}"
|
||||||
)
|
)
|
||||||
|
|
||||||
if place_in_container is not None:
|
# If we have a parent, we validate the place is accepted by this container.
|
||||||
parent_element_type.validate_place_in_container(
|
parent_element.get_type().validate_place_in_container(
|
||||||
place_in_container, parent_element
|
place_in_container, parent_element
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
if self.is_multi_page_element != page.shared:
|
||||||
|
raise ValidationError(
|
||||||
|
"This element type can't be added as root of a "
|
||||||
|
f"{'an unshared' if self.is_multi_page_element else 'the shared'} "
|
||||||
|
"page."
|
||||||
)
|
)
|
||||||
|
|
||||||
return values
|
|
||||||
|
|
||||||
def after_create(self, instance: ElementSubClass, values: Dict):
|
def after_create(self, instance: ElementSubClass, values: Dict):
|
||||||
"""
|
"""
|
||||||
This hook is called right after the element has been created.
|
This hook is called right after the element has been created.
|
||||||
|
|
|
@ -0,0 +1,79 @@
|
||||||
|
# Generated by Django 5.0.9 on 2024-10-17 08:01
|
||||||
|
|
||||||
|
import django.db.models.deletion
|
||||||
|
from django.db import migrations, models
|
||||||
|
|
||||||
|
|
||||||
|
class Migration(migrations.Migration):
|
||||||
|
dependencies = [
|
||||||
|
("builder", "0041_builder_login_page_page_role_type_page_roles_and_more"),
|
||||||
|
]
|
||||||
|
|
||||||
|
operations = [
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="FooterElement",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"element_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="builder.element",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"share_type",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("all", "All"),
|
||||||
|
("only", "Only"),
|
||||||
|
("except", "Except"),
|
||||||
|
],
|
||||||
|
default="all",
|
||||||
|
max_length=10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("pages", models.ManyToManyField(blank=True, to="builder.page")),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
bases=("builder.element",),
|
||||||
|
),
|
||||||
|
migrations.CreateModel(
|
||||||
|
name="HeaderElement",
|
||||||
|
fields=[
|
||||||
|
(
|
||||||
|
"element_ptr",
|
||||||
|
models.OneToOneField(
|
||||||
|
auto_created=True,
|
||||||
|
on_delete=django.db.models.deletion.CASCADE,
|
||||||
|
parent_link=True,
|
||||||
|
primary_key=True,
|
||||||
|
serialize=False,
|
||||||
|
to="builder.element",
|
||||||
|
),
|
||||||
|
),
|
||||||
|
(
|
||||||
|
"share_type",
|
||||||
|
models.CharField(
|
||||||
|
choices=[
|
||||||
|
("all", "All"),
|
||||||
|
("only", "Only"),
|
||||||
|
("except", "Except"),
|
||||||
|
],
|
||||||
|
default="all",
|
||||||
|
max_length=10,
|
||||||
|
),
|
||||||
|
),
|
||||||
|
("pages", models.ManyToManyField(blank=True, to="builder.page")),
|
||||||
|
],
|
||||||
|
options={
|
||||||
|
"abstract": False,
|
||||||
|
},
|
||||||
|
bases=("builder.element",),
|
||||||
|
),
|
||||||
|
]
|
|
@ -51,20 +51,34 @@ class PageHandler:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if base_queryset is None:
|
if base_queryset is None:
|
||||||
base_queryset = Page.objects
|
base_queryset = Page.objects_with_shared
|
||||||
|
|
||||||
try:
|
try:
|
||||||
return base_queryset.select_related("builder", "builder__workspace").get(
|
return base_queryset.select_related("builder__workspace").get(id=page_id)
|
||||||
id=page_id
|
|
||||||
)
|
|
||||||
except Page.DoesNotExist:
|
except Page.DoesNotExist:
|
||||||
raise PageDoesNotExist()
|
raise PageDoesNotExist()
|
||||||
|
|
||||||
def get_shared_page(self, builder: Builder) -> Page:
|
def get_shared_page(self, builder: Builder) -> Page:
|
||||||
return Page.objects.select_related("builder", "builder__workspace").get(
|
"""
|
||||||
|
Returns the shared page for the given builder.
|
||||||
|
"""
|
||||||
|
|
||||||
|
return Page.objects_with_shared.select_related("builder__workspace").get(
|
||||||
builder=builder, shared=True
|
builder=builder, shared=True
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_pages(self, builder, base_queryset: Optional[QuerySet] = None):
|
||||||
|
"""
|
||||||
|
Returns all the page in the current builder.
|
||||||
|
"""
|
||||||
|
|
||||||
|
if base_queryset is None:
|
||||||
|
base_queryset = Page.objects_with_shared.all()
|
||||||
|
|
||||||
|
return base_queryset.filter(builder=builder).select_related(
|
||||||
|
"builder__workspace"
|
||||||
|
)
|
||||||
|
|
||||||
def create_shared_page(self, builder: Builder) -> Page:
|
def create_shared_page(self, builder: Builder) -> Page:
|
||||||
"""
|
"""
|
||||||
Creates the shared page of the given builder.
|
Creates the shared page of the given builder.
|
||||||
|
@ -153,7 +167,7 @@ class PageHandler:
|
||||||
self.is_page_path_unique(
|
self.is_page_path_unique(
|
||||||
page.builder,
|
page.builder,
|
||||||
path,
|
path,
|
||||||
base_queryset=Page.objects.exclude(
|
base_queryset=Page.objects_with_shared.exclude(
|
||||||
id=page.id
|
id=page.id
|
||||||
), # We don't want to conflict with the current page
|
), # We don't want to conflict with the current page
|
||||||
raises=True,
|
raises=True,
|
||||||
|
@ -188,7 +202,7 @@ class PageHandler:
|
||||||
"""
|
"""
|
||||||
|
|
||||||
if base_qs is None:
|
if base_qs is None:
|
||||||
base_qs = Page.objects.filter(builder=builder, shared=False)
|
base_qs = Page.objects.filter(builder=builder)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
full_order = Page.order_objects(base_qs, order)
|
full_order = Page.order_objects(base_qs, order)
|
||||||
|
@ -345,7 +359,7 @@ class PageHandler:
|
||||||
:return: If the path is unique
|
:return: If the path is unique
|
||||||
"""
|
"""
|
||||||
|
|
||||||
queryset = Page.objects if base_queryset is None else base_queryset
|
queryset = Page.objects_with_shared if base_queryset is None else base_queryset
|
||||||
|
|
||||||
existing_paths = queryset.filter(builder=builder).values_list("path", flat=True)
|
existing_paths = queryset.filter(builder=builder).values_list("path", flat=True)
|
||||||
|
|
||||||
|
|
|
@ -20,6 +20,16 @@ if typing.TYPE_CHECKING:
|
||||||
from baserow.contrib.builder.models import Builder
|
from baserow.contrib.builder.models import Builder
|
||||||
|
|
||||||
|
|
||||||
|
class PageWithoutSharedManager(models.Manager):
|
||||||
|
"""
|
||||||
|
Manager for the Page model.
|
||||||
|
Excludes by default the shared page.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def get_queryset(self):
|
||||||
|
return super().get_queryset().filter(shared=False)
|
||||||
|
|
||||||
|
|
||||||
class Page(
|
class Page(
|
||||||
HierarchicalModelMixin,
|
HierarchicalModelMixin,
|
||||||
TrashableModelMixin,
|
TrashableModelMixin,
|
||||||
|
@ -36,6 +46,9 @@ class Page(
|
||||||
ALLOW_ALL_EXCEPT = "allow_all_except"
|
ALLOW_ALL_EXCEPT = "allow_all_except"
|
||||||
DISALLOW_ALL_EXCEPT = "disallow_all_except"
|
DISALLOW_ALL_EXCEPT = "disallow_all_except"
|
||||||
|
|
||||||
|
objects = PageWithoutSharedManager()
|
||||||
|
objects_with_shared = models.Manager()
|
||||||
|
|
||||||
builder = models.ForeignKey("builder.Builder", on_delete=models.CASCADE)
|
builder = models.ForeignKey("builder.Builder", on_delete=models.CASCADE)
|
||||||
order = models.PositiveIntegerField()
|
order = models.PositiveIntegerField()
|
||||||
name = models.CharField(max_length=255)
|
name = models.CharField(max_length=255)
|
||||||
|
|
|
@ -37,8 +37,7 @@ class PageService:
|
||||||
:return: The model instance of the Page
|
:return: The model instance of the Page
|
||||||
"""
|
"""
|
||||||
|
|
||||||
base_queryset = Page.objects.select_related("builder", "builder__workspace")
|
page = self.handler.get_page(page_id)
|
||||||
page = self.handler.get_page(page_id, base_queryset=base_queryset)
|
|
||||||
|
|
||||||
CoreHandler().check_permissions(
|
CoreHandler().check_permissions(
|
||||||
user,
|
user,
|
||||||
|
@ -148,7 +147,8 @@ class PageService:
|
||||||
context=builder,
|
context=builder,
|
||||||
)
|
)
|
||||||
|
|
||||||
all_pages = Page.objects.filter(builder_id=builder.id, shared=False)
|
all_pages = self.handler.get_pages(builder, base_queryset=Page.objects)
|
||||||
|
|
||||||
user_pages = CoreHandler().filter_queryset(
|
user_pages = CoreHandler().filter_queryset(
|
||||||
user,
|
user,
|
||||||
OrderPagesBuilderOperationType.type,
|
OrderPagesBuilderOperationType.type,
|
||||||
|
|
|
@ -1620,7 +1620,7 @@ def test_dispatch_data_sources_with_formula_using_datasource_calling_a_shared_da
|
||||||
integration = data_fixture.create_local_baserow_integration(
|
integration = data_fixture.create_local_baserow_integration(
|
||||||
user=user, application=builder
|
user=user, application=builder
|
||||||
)
|
)
|
||||||
shared_page = builder.page_set.first()
|
shared_page = builder.shared_page
|
||||||
page = data_fixture.create_builder_page(user=user, builder=builder)
|
page = data_fixture.create_builder_page(user=user, builder=builder)
|
||||||
|
|
||||||
data_source2 = data_fixture.create_builder_local_baserow_get_row_data_source(
|
data_source2 = data_fixture.create_builder_local_baserow_get_row_data_source(
|
||||||
|
@ -1686,7 +1686,7 @@ def test_dispatch_only_shared_data_sources(data_fixture, api_client):
|
||||||
integration = data_fixture.create_local_baserow_integration(
|
integration = data_fixture.create_local_baserow_integration(
|
||||||
user=user, application=builder
|
user=user, application=builder
|
||||||
)
|
)
|
||||||
shared_page = builder.page_set.first()
|
shared_page = builder.shared_page
|
||||||
page = data_fixture.create_builder_page(user=user, builder=builder)
|
page = data_fixture.create_builder_page(user=user, builder=builder)
|
||||||
|
|
||||||
shared_data_source = data_fixture.create_builder_local_baserow_get_row_data_source(
|
shared_data_source = data_fixture.create_builder_local_baserow_get_row_data_source(
|
||||||
|
|
|
@ -129,9 +129,12 @@ def test_get_public_builder_by_domain_name(api_client, data_fixture):
|
||||||
|
|
||||||
del response_json["theme"] # We are not testing the theme response here.
|
del response_json["theme"] # We are not testing the theme response here.
|
||||||
|
|
||||||
assert builder_to.page_set.filter(shared=True).count() == 1
|
assert (
|
||||||
|
builder_to.page_set(manager="objects_with_shared").filter(shared=True).count()
|
||||||
|
== 1
|
||||||
|
)
|
||||||
|
|
||||||
shared_page = builder_to.page_set.get(shared=True)
|
shared_page = builder_to.shared_page
|
||||||
|
|
||||||
assert response_json == {
|
assert response_json == {
|
||||||
"favicon_file": UserFileSerializer(builder_to.favicon_file).data,
|
"favicon_file": UserFileSerializer(builder_to.favicon_file).data,
|
||||||
|
@ -255,9 +258,12 @@ def test_get_public_builder_by_id(api_client, data_fixture):
|
||||||
|
|
||||||
del response_json["theme"] # We are not testing the theme response here.
|
del response_json["theme"] # We are not testing the theme response here.
|
||||||
|
|
||||||
assert page.builder.page_set.filter(shared=True).count() == 1
|
assert (
|
||||||
|
page.builder.page_set(manager="objects_with_shared").filter(shared=True).count()
|
||||||
|
== 1
|
||||||
|
)
|
||||||
|
|
||||||
shared_page = page.builder.page_set.get(shared=True)
|
shared_page = page.builder.shared_page
|
||||||
|
|
||||||
assert response_json == {
|
assert response_json == {
|
||||||
"favicon_file": UserFileSerializer(page.builder.favicon_file).data,
|
"favicon_file": UserFileSerializer(page.builder.favicon_file).data,
|
||||||
|
|
|
@ -359,7 +359,7 @@ def test_update_page_page_does_not_exist(api_client, data_fixture):
|
||||||
def test_update_shared_page(api_client, data_fixture):
|
def test_update_shared_page(api_client, data_fixture):
|
||||||
user, token = data_fixture.create_user_and_token()
|
user, token = data_fixture.create_user_and_token()
|
||||||
builder = data_fixture.create_builder_application(user=user)
|
builder = data_fixture.create_builder_application(user=user)
|
||||||
shared_page = builder.page_set.get(shared=True)
|
shared_page = builder.shared_page
|
||||||
|
|
||||||
url = reverse("api:builder:pages:item", kwargs={"page_id": shared_page.id})
|
url = reverse("api:builder:pages:item", kwargs={"page_id": shared_page.id})
|
||||||
response = api_client.patch(
|
response = api_client.patch(
|
||||||
|
@ -610,7 +610,7 @@ def test_order_pages_page_not_in_builder(api_client, data_fixture):
|
||||||
def test_order_pages_shared_page(api_client, data_fixture):
|
def test_order_pages_shared_page(api_client, data_fixture):
|
||||||
user, token = data_fixture.create_user_and_token()
|
user, token = data_fixture.create_user_and_token()
|
||||||
builder = data_fixture.create_builder_application(user=user)
|
builder = data_fixture.create_builder_application(user=user)
|
||||||
shared_page = builder.page_set.get(shared=True)
|
shared_page = builder.shared_page
|
||||||
page_one = data_fixture.create_builder_page(builder=builder, order=1)
|
page_one = data_fixture.create_builder_page(builder=builder, order=1)
|
||||||
|
|
||||||
url = reverse(
|
url = reverse(
|
||||||
|
@ -698,7 +698,7 @@ def test_delete_page_page_not_exist(api_client, data_fixture):
|
||||||
def test_delete_shared_page(api_client, data_fixture):
|
def test_delete_shared_page(api_client, data_fixture):
|
||||||
user, token = data_fixture.create_user_and_token()
|
user, token = data_fixture.create_user_and_token()
|
||||||
builder = data_fixture.create_builder_application(user=user)
|
builder = data_fixture.create_builder_application(user=user)
|
||||||
shared_page = builder.page_set.get(shared=True)
|
shared_page = builder.shared_page
|
||||||
|
|
||||||
url = reverse("api:builder:pages:item", kwargs={"page_id": shared_page.id})
|
url = reverse("api:builder:pages:item", kwargs={"page_id": shared_page.id})
|
||||||
response = api_client.delete(
|
response = api_client.delete(
|
||||||
|
|
|
@ -174,7 +174,7 @@ def test_get_builder_application(api_client, data_fixture):
|
||||||
"login_page_id": None,
|
"login_page_id": None,
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
{
|
||||||
"id": application.page_set.get(shared=True).id,
|
"id": application.shared_page.id,
|
||||||
"builder_id": application.id,
|
"builder_id": application.id,
|
||||||
"order": 1,
|
"order": 1,
|
||||||
"name": "__shared__",
|
"name": "__shared__",
|
||||||
|
@ -233,7 +233,7 @@ def test_list_builder_applications(api_client, data_fixture):
|
||||||
"login_page_id": None,
|
"login_page_id": None,
|
||||||
"pages": [
|
"pages": [
|
||||||
{
|
{
|
||||||
"id": application.page_set.get(shared=True).id,
|
"id": application.shared_page.id,
|
||||||
"builder_id": application.id,
|
"builder_id": application.id,
|
||||||
"order": 1,
|
"order": 1,
|
||||||
"name": "__shared__",
|
"name": "__shared__",
|
||||||
|
|
|
@ -48,7 +48,7 @@ def test_validate_login_page_id_raises_error_if_shared_page(
|
||||||
builder = builder_fixture["builder"]
|
builder = builder_fixture["builder"]
|
||||||
|
|
||||||
# Set the builder's page to be the shared page
|
# Set the builder's page to be the shared page
|
||||||
shared_page = builder.page_set.get(shared=True)
|
shared_page = builder.page_set(manager="objects_with_shared").get(shared=True)
|
||||||
response = api_client.patch(
|
response = api_client.patch(
|
||||||
reverse("api:applications:item", kwargs={"application_id": builder.id}),
|
reverse("api:applications:item", kwargs={"application_id": builder.id}),
|
||||||
{"login_page_id": shared_page.id},
|
{"login_page_id": shared_page.id},
|
||||||
|
|
|
@ -717,7 +717,7 @@ def test_dispatch_local_baserow_update_row_workflow_action_using_formula_with_da
|
||||||
integration = data_fixture.create_local_baserow_integration(
|
integration = data_fixture.create_local_baserow_integration(
|
||||||
user=user, application=builder
|
user=user, application=builder
|
||||||
)
|
)
|
||||||
shared_page = builder.page_set.first()
|
shared_page = builder.shared_page
|
||||||
|
|
||||||
shared_data_source = data_fixture.create_builder_local_baserow_get_row_data_source(
|
shared_data_source = data_fixture.create_builder_local_baserow_get_row_data_source(
|
||||||
user=user,
|
user=user,
|
||||||
|
|
|
@ -94,7 +94,7 @@ def test_get_data_sources(data_fixture, specific):
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_get_data_sources_with_shared(data_fixture):
|
def test_get_data_sources_with_shared(data_fixture):
|
||||||
page = data_fixture.create_builder_page()
|
page = data_fixture.create_builder_page()
|
||||||
shared_page = page.builder.page_set.get(shared=True)
|
shared_page = page.builder.shared_page
|
||||||
data_source1 = data_fixture.create_builder_local_baserow_get_row_data_source(
|
data_source1 = data_fixture.create_builder_local_baserow_get_row_data_source(
|
||||||
page=page
|
page=page
|
||||||
)
|
)
|
||||||
|
|
|
@ -186,10 +186,7 @@ def test_domain_publishing(data_fixture):
|
||||||
assert domain1.published_to is not None
|
assert domain1.published_to is not None
|
||||||
assert domain1.published_to.workspace is None
|
assert domain1.published_to.workspace is None
|
||||||
assert domain1.published_to.page_set.count() == builder.page_set.count()
|
assert domain1.published_to.page_set.count() == builder.page_set.count()
|
||||||
assert (
|
assert domain1.published_to.page_set.first().element_set.count() == 2
|
||||||
domain1.published_to.page_set.exclude(shared=True).first().element_set.count()
|
|
||||||
== 2
|
|
||||||
)
|
|
||||||
|
|
||||||
assert Builder.objects.count() == 2
|
assert Builder.objects.count() == 2
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,7 @@
|
||||||
from decimal import Decimal
|
from decimal import Decimal
|
||||||
|
|
||||||
import pytest
|
import pytest
|
||||||
|
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||||||
|
|
||||||
from baserow.contrib.builder.elements.element_types import (
|
from baserow.contrib.builder.elements.element_types import (
|
||||||
ColumnElementType,
|
ColumnElementType,
|
||||||
|
@ -27,9 +28,13 @@ def pytest_generate_tests(metafunc):
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_create_element(data_fixture, element_type):
|
def test_create_element(data_fixture, element_type):
|
||||||
page = data_fixture.create_builder_page()
|
page = data_fixture.create_builder_page()
|
||||||
|
shared_page = page.builder.shared_page
|
||||||
|
|
||||||
pytest_params = element_type.get_pytest_params(data_fixture)
|
pytest_params = element_type.get_pytest_params(data_fixture)
|
||||||
|
|
||||||
|
if element_type.is_multi_page_element:
|
||||||
|
page = shared_page
|
||||||
|
|
||||||
element = ElementHandler().create_element(element_type, page=page, **pytest_params)
|
element = ElementHandler().create_element(element_type, page=page, **pytest_params)
|
||||||
|
|
||||||
assert element.page.id == page.id
|
assert element.page.id == page.id
|
||||||
|
@ -41,6 +46,34 @@ def test_create_element(data_fixture, element_type):
|
||||||
assert Element.objects.count() == 1
|
assert Element.objects.count() == 1
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
def test_create_element_and_shared_page(data_fixture):
|
||||||
|
page = data_fixture.create_builder_page()
|
||||||
|
shared_page = page.builder.shared_page
|
||||||
|
|
||||||
|
regular_element_type = next(
|
||||||
|
filter(lambda t: not t.is_multi_page_element, element_type_registry.get_all())
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(DRFValidationError):
|
||||||
|
ElementHandler().create_element(
|
||||||
|
regular_element_type,
|
||||||
|
page=shared_page,
|
||||||
|
**regular_element_type.get_pytest_params(data_fixture),
|
||||||
|
)
|
||||||
|
|
||||||
|
shared_element_type = next(
|
||||||
|
filter(lambda t: t.is_multi_page_element, element_type_registry.get_all())
|
||||||
|
)
|
||||||
|
|
||||||
|
with pytest.raises(DRFValidationError):
|
||||||
|
ElementHandler().create_element(
|
||||||
|
shared_element_type,
|
||||||
|
page=page,
|
||||||
|
**regular_element_type.get_pytest_params(data_fixture),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_get_element(data_fixture):
|
def test_get_element(data_fixture):
|
||||||
element = data_fixture.create_builder_heading_element()
|
element = data_fixture.create_builder_heading_element()
|
||||||
|
|
|
@ -26,6 +26,11 @@ def pytest_generate_tests(metafunc):
|
||||||
def test_create_element(element_created_mock, data_fixture, element_type):
|
def test_create_element(element_created_mock, data_fixture, element_type):
|
||||||
user = data_fixture.create_user()
|
user = data_fixture.create_user()
|
||||||
page = data_fixture.create_builder_page(user=user)
|
page = data_fixture.create_builder_page(user=user)
|
||||||
|
shared_page = page.builder.shared_page
|
||||||
|
|
||||||
|
if element_type.is_multi_page_element:
|
||||||
|
page = shared_page
|
||||||
|
|
||||||
element1 = data_fixture.create_builder_heading_element(page=page, order="1.0000")
|
element1 = data_fixture.create_builder_heading_element(page=page, order="1.0000")
|
||||||
element3 = data_fixture.create_builder_heading_element(page=page, order="2.0000")
|
element3 = data_fixture.create_builder_heading_element(page=page, order="2.0000")
|
||||||
|
|
||||||
|
|
|
@ -290,10 +290,13 @@ def test_form_container_element_import_export_formula(data_fixture):
|
||||||
element_type.type
|
element_type.type
|
||||||
for element_type in element_type_registry.get_all()
|
for element_type in element_type_registry.get_all()
|
||||||
if element_type.type != FormContainerElementType.type
|
if element_type.type != FormContainerElementType.type
|
||||||
|
and not element_type.is_multi_page_element
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_form_container_child_types_allowed(allowed_element_type):
|
def test_form_container_child_types_allowed(allowed_element_type):
|
||||||
assert allowed_element_type in FormContainerElementType().child_types_allowed
|
assert allowed_element_type in [
|
||||||
|
e.type for e in FormContainerElementType().child_types_allowed
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
@ -1347,10 +1350,13 @@ def test_choice_element_integer_option_values(data_fixture):
|
||||||
element_type.type
|
element_type.type
|
||||||
for element_type in element_type_registry.get_all()
|
for element_type in element_type_registry.get_all()
|
||||||
if element_type.type != ColumnElementType.type
|
if element_type.type != ColumnElementType.type
|
||||||
|
and not element_type.is_multi_page_element
|
||||||
],
|
],
|
||||||
)
|
)
|
||||||
def test_column_container_child_types_allowed(allowed_element_type):
|
def test_column_container_child_types_allowed(allowed_element_type):
|
||||||
assert allowed_element_type in ColumnElementType().child_types_allowed
|
assert allowed_element_type in [
|
||||||
|
e.type for e in ColumnElementType().child_types_allowed
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
@ -1513,7 +1519,7 @@ def test_repeat_element_import_export(data_fixture):
|
||||||
imported_field = imported_table.field_set.get()
|
imported_field = imported_table.field_set.get()
|
||||||
|
|
||||||
# Pluck out the imported builder records.
|
# Pluck out the imported builder records.
|
||||||
imported_page = imported_builder.page_set.filter(shared=False).all()[0]
|
imported_page = imported_builder.page_set.all()[0]
|
||||||
imported_data_source = imported_page.datasource_set.get()
|
imported_data_source = imported_page.datasource_set.get()
|
||||||
imported_root_repeat = imported_page.element_set.get(
|
imported_root_repeat = imported_page.element_set.get(
|
||||||
parent_element_id=None
|
parent_element_id=None
|
||||||
|
|
|
@ -0,0 +1,93 @@
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from baserow.contrib.builder.elements.element_types import (
|
||||||
|
FooterElementType,
|
||||||
|
HeaderElementType,
|
||||||
|
)
|
||||||
|
from baserow.contrib.builder.elements.handler import ElementHandler
|
||||||
|
from baserow.contrib.builder.elements.registries import element_type_registry
|
||||||
|
|
||||||
|
|
||||||
|
def test_header_footer_child_types_allowed():
|
||||||
|
assert sorted([e.type for e in HeaderElementType().child_types_allowed]) == sorted(
|
||||||
|
[
|
||||||
|
element_type.type
|
||||||
|
for element_type in element_type_registry.get_all()
|
||||||
|
if not element_type.is_multi_page_element
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
assert sorted([e.type for e in FooterElementType().child_types_allowed]) == sorted(
|
||||||
|
[
|
||||||
|
element_type.type
|
||||||
|
for element_type in element_type_registry.get_all()
|
||||||
|
if not element_type.is_multi_page_element
|
||||||
|
]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# Test prepare value
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"element_type", [HeaderElementType.type, FooterElementType.type]
|
||||||
|
)
|
||||||
|
def test_header_footer_prepare_value_for_db(data_fixture, element_type):
|
||||||
|
page = data_fixture.create_builder_page()
|
||||||
|
page1 = data_fixture.create_builder_page(builder=page.builder)
|
||||||
|
page2 = data_fixture.create_builder_page(builder=page.builder)
|
||||||
|
page3 = data_fixture.create_builder_page(builder=page.builder)
|
||||||
|
page4 = data_fixture.create_builder_page()
|
||||||
|
shared_page = page.builder.shared_page
|
||||||
|
|
||||||
|
element_type = element_type_registry.get(element_type)
|
||||||
|
|
||||||
|
created_element = ElementHandler().create_element(
|
||||||
|
element_type,
|
||||||
|
page=shared_page,
|
||||||
|
share_type="only",
|
||||||
|
pages=[page1, page2, page4, shared_page],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert sorted([p.id for p in created_element.pages.all()]) == sorted(
|
||||||
|
[page1.id, page2.id]
|
||||||
|
)
|
||||||
|
|
||||||
|
updated_element = ElementHandler().update_element(
|
||||||
|
created_element,
|
||||||
|
pages=[page1, page4, shared_page],
|
||||||
|
)
|
||||||
|
|
||||||
|
assert sorted([p.id for p in updated_element.pages.all()]) == sorted([page1.id])
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.django_db
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"element_type", [HeaderElementType.type, FooterElementType.type]
|
||||||
|
)
|
||||||
|
def test_header_footer_import_with_id_mapping(data_fixture, element_type):
|
||||||
|
page = data_fixture.create_builder_page()
|
||||||
|
page42 = data_fixture.create_builder_page()
|
||||||
|
page43 = data_fixture.create_builder_page()
|
||||||
|
|
||||||
|
SERIALIZED_HEADER = {
|
||||||
|
"id": 42,
|
||||||
|
"type": element_type,
|
||||||
|
"share_type": "only",
|
||||||
|
"parent_element_id": None,
|
||||||
|
"pages": [42, 43],
|
||||||
|
}
|
||||||
|
|
||||||
|
cache = {}
|
||||||
|
id_mapping = {"builder_pages": {42: page42, 43: page43}}
|
||||||
|
|
||||||
|
created_element = ElementHandler().import_element(
|
||||||
|
page,
|
||||||
|
SERIALIZED_HEADER,
|
||||||
|
id_mapping,
|
||||||
|
cache=cache,
|
||||||
|
)
|
||||||
|
|
||||||
|
# We keep only the pages that are in the same builder
|
||||||
|
assert sorted([p.id for p in created_element.pages.all()]) == sorted(
|
||||||
|
[page42.id, page43.id]
|
||||||
|
)
|
|
@ -93,12 +93,7 @@ def test_export_import_record_selector_element(data_fixture):
|
||||||
import_export_config=config,
|
import_export_config=config,
|
||||||
)
|
)
|
||||||
imported_builder = imported_apps[-1]
|
imported_builder = imported_apps[-1]
|
||||||
imported_element = (
|
imported_element = imported_builder.page_set.first().element_set.first().specific
|
||||||
imported_builder.page_set.filter(shared=False)
|
|
||||||
.first()
|
|
||||||
.element_set.first()
|
|
||||||
.specific
|
|
||||||
)
|
|
||||||
|
|
||||||
# Check that the formula for option name suffix was updated with the new mapping
|
# Check that the formula for option name suffix was updated with the new mapping
|
||||||
import_option_name_suffix = imported_element.option_name_suffix
|
import_option_name_suffix = imported_element.option_name_suffix
|
||||||
|
|
|
@ -94,7 +94,7 @@ def test_delete_page(data_fixture):
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_delete_shared_page(data_fixture):
|
def test_delete_shared_page(data_fixture):
|
||||||
page = data_fixture.create_builder_page()
|
page = data_fixture.create_builder_page()
|
||||||
shared_page = page.builder.page_set.get(shared=True)
|
shared_page = page.builder.shared_page
|
||||||
|
|
||||||
with pytest.raises(SharedPageIsReadOnly):
|
with pytest.raises(SharedPageIsReadOnly):
|
||||||
PageHandler().delete_page(shared_page)
|
PageHandler().delete_page(shared_page)
|
||||||
|
@ -114,7 +114,7 @@ def test_update_page(data_fixture):
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_update_shared_page(data_fixture):
|
def test_update_shared_page(data_fixture):
|
||||||
page = data_fixture.create_builder_page(name="test")
|
page = data_fixture.create_builder_page(name="test")
|
||||||
shared_page = page.builder.page_set.get(shared=True)
|
shared_page = page.builder.shared_page
|
||||||
|
|
||||||
with pytest.raises(SharedPageIsReadOnly):
|
with pytest.raises(SharedPageIsReadOnly):
|
||||||
PageHandler().update_page(shared_page, name="new")
|
PageHandler().update_page(shared_page, name="new")
|
||||||
|
@ -158,7 +158,7 @@ def test_order_pages(data_fixture):
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_order_pages_page_not_in_builder(data_fixture):
|
def test_order_pages_page_not_in_builder(data_fixture):
|
||||||
builder = data_fixture.create_builder_application()
|
builder = data_fixture.create_builder_application()
|
||||||
shared_page = builder.page_set.get(shared=True)
|
shared_page = builder.shared_page
|
||||||
page_one = data_fixture.create_builder_page(builder=builder, order=1)
|
page_one = data_fixture.create_builder_page(builder=builder, order=1)
|
||||||
page_two = data_fixture.create_builder_page(builder=builder, order=2)
|
page_two = data_fixture.create_builder_page(builder=builder, order=2)
|
||||||
|
|
||||||
|
@ -189,7 +189,7 @@ def test_duplicate_page(data_fixture):
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
def test_duplicate_shared_page(data_fixture):
|
def test_duplicate_shared_page(data_fixture):
|
||||||
page = data_fixture.create_builder_page()
|
page = data_fixture.create_builder_page()
|
||||||
shared_page = page.builder.page_set.get(shared=True)
|
shared_page = page.builder.shared_page
|
||||||
|
|
||||||
with pytest.raises(SharedPageIsReadOnly):
|
with pytest.raises(SharedPageIsReadOnly):
|
||||||
PageHandler().duplicate_page(shared_page)
|
PageHandler().duplicate_page(shared_page)
|
||||||
|
|
|
@ -60,11 +60,11 @@ def test_builder_application_type_init_application(data_fixture):
|
||||||
user = data_fixture.create_user()
|
user = data_fixture.create_user()
|
||||||
builder = data_fixture.create_builder_application(user=user)
|
builder = data_fixture.create_builder_application(user=user)
|
||||||
|
|
||||||
assert Page.objects.count() == 1 # The shared page must exists
|
assert Page.objects.count() == 0
|
||||||
|
|
||||||
BuilderApplicationType().init_application(user, builder)
|
BuilderApplicationType().init_application(user, builder)
|
||||||
|
|
||||||
assert Page.objects.count() == 3 # With demo data
|
assert Page.objects.count() == 2 # With demo data
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.django_db
|
@pytest.mark.django_db
|
||||||
|
@ -119,7 +119,7 @@ def test_builder_application_export(data_fixture):
|
||||||
user = data_fixture.create_user()
|
user = data_fixture.create_user()
|
||||||
builder = data_fixture.create_builder_application(user=user)
|
builder = data_fixture.create_builder_application(user=user)
|
||||||
|
|
||||||
shared_page = builder.page_set.get(shared=True)
|
shared_page = builder.shared_page
|
||||||
page1 = data_fixture.create_builder_page(builder=builder)
|
page1 = data_fixture.create_builder_page(builder=builder)
|
||||||
page2 = data_fixture.create_builder_page(builder=builder)
|
page2 = data_fixture.create_builder_page(builder=builder)
|
||||||
|
|
||||||
|
@ -1002,8 +1002,11 @@ def test_builder_application_import(data_fixture):
|
||||||
)
|
)
|
||||||
|
|
||||||
assert builder.id != serialized_values["id"]
|
assert builder.id != serialized_values["id"]
|
||||||
assert builder.page_set.exclude(shared=True).count() == 2
|
assert builder.page_set.count() == 2
|
||||||
assert builder.page_set.filter(shared=True).count() == 1
|
# ensure we have the shared page even if it's not in the reference
|
||||||
|
assert (
|
||||||
|
builder.page_set(manager="objects_with_shared").filter(shared=True).count() == 1
|
||||||
|
)
|
||||||
|
|
||||||
assert builder.integrations.count() == 1
|
assert builder.integrations.count() == 1
|
||||||
first_integration = builder.integrations.first().specific
|
first_integration = builder.integrations.first().specific
|
||||||
|
@ -1011,7 +1014,7 @@ def test_builder_application_import(data_fixture):
|
||||||
|
|
||||||
assert builder.user_sources.count() == 1
|
assert builder.user_sources.count() == 1
|
||||||
|
|
||||||
[page1, page2] = builder.page_set.exclude(shared=True)
|
[page1, page2] = builder.page_set.all()
|
||||||
|
|
||||||
assert page1.element_set.count() == 6
|
assert page1.element_set.count() == 6
|
||||||
assert page2.element_set.count() == 1
|
assert page2.element_set.count() == 1
|
||||||
|
@ -1371,7 +1374,7 @@ def test_builder_application_imports_correct_default_roles(data_fixture):
|
||||||
workspace, serialized_values, config, {}
|
workspace, serialized_values, config, {}
|
||||||
)
|
)
|
||||||
|
|
||||||
new_element = builder.page_set.exclude(shared=True)[0].element_set.all()[0]
|
new_element = builder.page_set.first().element_set.all()[0]
|
||||||
new_user_source = builder.user_sources.all()[0]
|
new_user_source = builder.user_sources.all()[0]
|
||||||
|
|
||||||
# Ensure the "old" Default User Role doesn't exist
|
# Ensure the "old" Default User Role doesn't exist
|
||||||
|
@ -1455,7 +1458,7 @@ def test_ensure_new_element_roles_are_sanitized_during_import_for_default_roles(
|
||||||
expected_roles = _expected_roles
|
expected_roles = _expected_roles
|
||||||
|
|
||||||
# Ensure new element has roles updated
|
# Ensure new element has roles updated
|
||||||
new_element = builder.page_set.exclude(shared=True)[0].element_set.all()[0]
|
new_element = builder.page_set.all()[0].element_set.all()[0]
|
||||||
for index, role in enumerate(new_element.roles):
|
for index, role in enumerate(new_element.roles):
|
||||||
# Default Role's User Source should have changed for new elements
|
# Default Role's User Source should have changed for new elements
|
||||||
if role.startswith(prefix):
|
if role.startswith(prefix):
|
||||||
|
@ -1532,5 +1535,5 @@ def test_ensure_new_element_roles_are_sanitized_during_import_for_roles(
|
||||||
workspace, serialized, config, {}
|
workspace, serialized, config, {}
|
||||||
)
|
)
|
||||||
|
|
||||||
new_element = builder.page_set.exclude(shared=True)[0].element_set.all()[0]
|
new_element = builder.page_set.all()[0].element_set.all()[0]
|
||||||
assert new_element.roles == expected_roles
|
assert new_element.roles == expected_roles
|
||||||
|
|
|
@ -901,7 +901,7 @@ def test_get_builder_used_property_names_returns_merged_property_names_integrati
|
||||||
integration = data_fixture.create_local_baserow_integration(
|
integration = data_fixture.create_local_baserow_integration(
|
||||||
user=user, application=builder
|
user=user, application=builder
|
||||||
)
|
)
|
||||||
shared_page = builder.page_set.get(shared=True)
|
shared_page = builder.shared_page
|
||||||
page = data_fixture.create_builder_page(builder=builder)
|
page = data_fixture.create_builder_page(builder=builder)
|
||||||
page2 = data_fixture.create_builder_page(builder=builder)
|
page2 = data_fixture.create_builder_page(builder=builder)
|
||||||
|
|
||||||
|
|
|
@ -182,7 +182,7 @@ def test_allow_if_template_permission_manager_filter_queryset(data_fixture):
|
||||||
workspace_2 = data_fixture.create_workspace()
|
workspace_2 = data_fixture.create_workspace()
|
||||||
data_fixture.create_template(workspace=workspace_2)
|
data_fixture.create_template(workspace=workspace_2)
|
||||||
application_2 = data_fixture.create_builder_application(workspace=workspace_2)
|
application_2 = data_fixture.create_builder_application(workspace=workspace_2)
|
||||||
shared_page_2 = application_2.page_set.get(shared=True)
|
shared_page_2 = application_2.shared_page
|
||||||
page_2 = data_fixture.create_builder_page(builder=application_2)
|
page_2 = data_fixture.create_builder_page(builder=application_2)
|
||||||
element_2 = data_fixture.create_builder_text_element(page=page_2)
|
element_2 = data_fixture.create_builder_text_element(page=page_2)
|
||||||
workflow_action_2 = data_fixture.create_local_baserow_update_row_workflow_action(
|
workflow_action_2 = data_fixture.create_local_baserow_update_row_workflow_action(
|
||||||
|
@ -230,7 +230,7 @@ def test_allow_if_template_permission_manager_filter_queryset(data_fixture):
|
||||||
tests_w1 = [
|
tests_w1 = [
|
||||||
(
|
(
|
||||||
ListPagesBuilderOperationType.type,
|
ListPagesBuilderOperationType.type,
|
||||||
Page.objects.filter(builder__workspace=workspace_2),
|
Page.objects_with_shared.filter(builder__workspace=workspace_2),
|
||||||
[shared_page_2.id, page_2.id],
|
[shared_page_2.id, page_2.id],
|
||||||
),
|
),
|
||||||
(
|
(
|
||||||
|
|
|
@ -603,7 +603,7 @@ def test_export_import_local_baserow_upsert_row_service(
|
||||||
imported_table = imported_database.table_set.get()
|
imported_table = imported_database.table_set.get()
|
||||||
imported_field = imported_table.field_set.get()
|
imported_field = imported_table.field_set.get()
|
||||||
|
|
||||||
imported_page = imported_builder.page_set.exclude(shared=True).get()
|
imported_page = imported_builder.page_set.get()
|
||||||
imported_data_source = imported_page.datasource_set.get()
|
imported_data_source = imported_page.datasource_set.get()
|
||||||
imported_integration = imported_builder.integrations.get()
|
imported_integration = imported_builder.integrations.get()
|
||||||
imported_upsert_row_service = LocalBaserowUpsertRow.objects.get(
|
imported_upsert_row_service = LocalBaserowUpsertRow.objects.get(
|
||||||
|
|
|
@ -169,7 +169,7 @@ def test_local_baserow_table_service_filterable_mixin_import_export(data_fixture
|
||||||
imported_select_option = imported_single_select_field.select_options.get()
|
imported_select_option = imported_single_select_field.select_options.get()
|
||||||
|
|
||||||
# Pluck out the imported builder records.
|
# Pluck out the imported builder records.
|
||||||
imported_page = imported_builder.page_set.filter(shared=False).get()
|
imported_page = imported_builder.page_set.get()
|
||||||
imported_datasource = imported_page.datasource_set.get()
|
imported_datasource = imported_page.datasource_set.get()
|
||||||
imported_filters = [
|
imported_filters = [
|
||||||
{"field_id": sf.field_id, "value": sf.value}
|
{"field_id": sf.field_id, "value": sf.value}
|
||||||
|
|
|
@ -0,0 +1,7 @@
|
||||||
|
{
|
||||||
|
"type": "feature",
|
||||||
|
"message": "[Builder] Add the multi-page header and footer containers",
|
||||||
|
"issue_number": 2486,
|
||||||
|
"bullet_points": [],
|
||||||
|
"created_at": "2024-10-29"
|
||||||
|
}
|
|
@ -58,7 +58,7 @@ test.describe("Builder page test suite", () => {
|
||||||
});
|
});
|
||||||
|
|
||||||
test("Can create an element from empty page", async ({ page }) => {
|
test("Can create an element from empty page", async ({ page }) => {
|
||||||
await page.getByText("Click to create first element").click();
|
await page.getByText("Click to create an element").click();
|
||||||
await page.getByText("Heading", { exact: true }).click();
|
await page.getByText("Heading", { exact: true }).click();
|
||||||
|
|
||||||
await expect(
|
await expect(
|
||||||
|
|
|
@ -957,7 +957,7 @@ def test_public_dispatch_data_source_with_ab_user_using_user_source(
|
||||||
refresh_token = user_source_user.get_refresh_token()
|
refresh_token = user_source_user.get_refresh_token()
|
||||||
access_token = refresh_token.access_token
|
access_token = refresh_token.access_token
|
||||||
|
|
||||||
published_page = domain1.published_to.page_set.exclude(shared=True).first()
|
published_page = domain1.published_to.page_set.first()
|
||||||
published_data_source = published_page.datasource_set.first()
|
published_data_source = published_page.datasource_set.first()
|
||||||
|
|
||||||
url = reverse(
|
url = reverse(
|
||||||
|
|
|
@ -65,7 +65,7 @@ import { mapActions } from 'vuex'
|
||||||
export default {
|
export default {
|
||||||
name: 'AuthFormElement',
|
name: 'AuthFormElement',
|
||||||
mixins: [element, form, error],
|
mixins: [element, form, error],
|
||||||
inject: ['page', 'builder'],
|
inject: ['elementPage', 'builder'],
|
||||||
props: {
|
props: {
|
||||||
/**
|
/**
|
||||||
* @type {Object}
|
* @type {Object}
|
||||||
|
@ -128,7 +128,7 @@ export default {
|
||||||
if (!found) {
|
if (!found) {
|
||||||
// If the user_source has been removed we need to update the element
|
// If the user_source has been removed we need to update the element
|
||||||
this.actionForceUpdateElement({
|
this.actionForceUpdateElement({
|
||||||
page: this.page,
|
page: this.elementPage,
|
||||||
element: this.element,
|
element: this.element,
|
||||||
values: { user_source_id: null },
|
values: { user_source_id: null },
|
||||||
})
|
})
|
||||||
|
|
|
@ -86,9 +86,10 @@ export class BuilderApplicationType extends ApplicationType {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async loadExtraData(builder, page, mode) {
|
async loadExtraData(builder, mode) {
|
||||||
const { store, $registry } = this.app
|
const { store, $registry } = this.app
|
||||||
if (!builder._loadedOnce) {
|
if (!builder._loadedOnce) {
|
||||||
|
const sharedPage = store.getters['page/getSharedPage'](builder)
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
store.dispatch('userSource/fetch', {
|
store.dispatch('userSource/fetch', {
|
||||||
application: builder,
|
application: builder,
|
||||||
|
@ -98,8 +99,12 @@ export class BuilderApplicationType extends ApplicationType {
|
||||||
}),
|
}),
|
||||||
// Fetch shared data sources
|
// Fetch shared data sources
|
||||||
store.dispatch('dataSource/fetch', {
|
store.dispatch('dataSource/fetch', {
|
||||||
page: store.getters['page/getSharedPage'](builder),
|
page: sharedPage,
|
||||||
}),
|
}),
|
||||||
|
store.dispatch('element/fetch', {
|
||||||
|
page: sharedPage,
|
||||||
|
}),
|
||||||
|
store.dispatch('workflowAction/fetch', { page: sharedPage }),
|
||||||
])
|
])
|
||||||
|
|
||||||
// Initialize application shared stuff like data sources
|
// Initialize application shared stuff like data sources
|
||||||
|
|
|
@ -19,8 +19,8 @@ export default {
|
||||||
components: { FormulaInputField },
|
components: { FormulaInputField },
|
||||||
mixins: [applicationContext],
|
mixins: [applicationContext],
|
||||||
inject: {
|
inject: {
|
||||||
page: {
|
elementPage: {
|
||||||
from: 'page',
|
from: 'elementPage',
|
||||||
},
|
},
|
||||||
builder: {
|
builder: {
|
||||||
from: 'builder',
|
from: 'builder',
|
||||||
|
@ -38,10 +38,12 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
dataSourceLoading() {
|
dataSourceLoading() {
|
||||||
return this.$store.getters['dataSource/getLoading'](this.page)
|
return this.$store.getters['dataSource/getLoading'](this.elementPage)
|
||||||
},
|
},
|
||||||
dataSourceContentLoading() {
|
dataSourceContentLoading() {
|
||||||
return this.$store.getters['dataSourceContent/getLoading'](this.page)
|
return this.$store.getters['dataSourceContent/getLoading'](
|
||||||
|
this.elementPage
|
||||||
|
)
|
||||||
},
|
},
|
||||||
dataProviders() {
|
dataProviders() {
|
||||||
return this.dataProvidersAllowed.map((dataProviderName) =>
|
return this.dataProvidersAllowed.map((dataProviderName) =>
|
||||||
|
|
|
@ -61,7 +61,16 @@ export default {
|
||||||
components: { DataSourceForm },
|
components: { DataSourceForm },
|
||||||
|
|
||||||
mixins: [modal, error],
|
mixins: [modal, error],
|
||||||
inject: ['builder', 'page'],
|
provide() {
|
||||||
|
// I know, it's not the page of the element but it's injected into the
|
||||||
|
// ApplicationBuilderFormulaInput for data source loading states,
|
||||||
|
// and we need the right page which can be in fact the data source page in this
|
||||||
|
// case, so it works.
|
||||||
|
// May be we could change the name of the elementPage but it would be only for
|
||||||
|
// this exception.
|
||||||
|
return { elementPage: this.dataSourcePage }
|
||||||
|
},
|
||||||
|
inject: ['builder', 'currentPage'],
|
||||||
props: {
|
props: {
|
||||||
dataSourceId: { type: Number, required: false, default: null },
|
dataSourceId: { type: Number, required: false, default: null },
|
||||||
},
|
},
|
||||||
|
@ -74,7 +83,9 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
dataSources() {
|
dataSources() {
|
||||||
return this.$store.getters['dataSource/getPageDataSources'](this.page)
|
return this.$store.getters['dataSource/getPageDataSources'](
|
||||||
|
this.currentPage
|
||||||
|
)
|
||||||
},
|
},
|
||||||
sharedPage() {
|
sharedPage() {
|
||||||
return this.$store.getters['page/getSharedPage'](this.builder)
|
return this.$store.getters['page/getSharedPage'](this.builder)
|
||||||
|
@ -108,7 +119,7 @@ export default {
|
||||||
// edited. Sometimes it's the shared page.
|
// edited. Sometimes it's the shared page.
|
||||||
dataSourcePage() {
|
dataSourcePage() {
|
||||||
if (!this.dataSource) {
|
if (!this.dataSource) {
|
||||||
return this.page
|
return this.currentPage
|
||||||
}
|
}
|
||||||
return this.$store.getters['page/getById'](
|
return this.$store.getters['page/getById'](
|
||||||
this.builder,
|
this.builder,
|
||||||
|
@ -116,7 +127,11 @@ export default {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
elements() {
|
elements() {
|
||||||
return this.$store.getters['element/getElementsOrdered'](this.page)
|
// This is used when we want to dispatch the data source update
|
||||||
|
return [
|
||||||
|
...this.$store.getters['element/getElementsOrdered'](this.currentPage),
|
||||||
|
...this.$store.getters['element/getElementsOrdered'](this.sharedPage),
|
||||||
|
]
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -145,7 +160,7 @@ export default {
|
||||||
try {
|
try {
|
||||||
if (this.create) {
|
if (this.create) {
|
||||||
const createdDataSource = await this.actionCreateDataSource({
|
const createdDataSource = await this.actionCreateDataSource({
|
||||||
page: this.page,
|
page: this.currentPage,
|
||||||
values,
|
values,
|
||||||
})
|
})
|
||||||
this.actualDataSourceId = createdDataSource.id
|
this.actualDataSourceId = createdDataSource.id
|
||||||
|
|
|
@ -17,18 +17,24 @@
|
||||||
:icon-tooltip="$t('dataSourceDropdown.shared')"
|
:icon-tooltip="$t('dataSourceDropdown.shared')"
|
||||||
>
|
>
|
||||||
</DropdownItem>
|
</DropdownItem>
|
||||||
<DropdownItem
|
<template v-if="localDataSources">
|
||||||
v-for="dataSource in pageDataSources"
|
<DropdownItem
|
||||||
:key="dataSource.id"
|
v-for="dataSource in localDataSources"
|
||||||
:name="getDataSourceLabel(dataSource)"
|
:key="dataSource.id"
|
||||||
:value="dataSource.id"
|
:name="getDataSourceLabel(dataSource)"
|
||||||
icon="iconoir-empty-page"
|
:value="dataSource.id"
|
||||||
:icon-tooltip="$t('dataSourceDropdown.pageOnly')"
|
icon="iconoir-empty-page"
|
||||||
>
|
:icon-tooltip="$t('dataSourceDropdown.pageOnly')"
|
||||||
</DropdownItem>
|
>
|
||||||
|
</DropdownItem
|
||||||
|
></template>
|
||||||
<template #emptyState>
|
<template #emptyState>
|
||||||
<slot name="emptyState"
|
<slot name="emptyState">
|
||||||
>{{ $t('dataSourceDropdown.noDataSources') }}
|
{{
|
||||||
|
isOnSharedPage
|
||||||
|
? $t('dataSourceDropdown.noSharedDataSources')
|
||||||
|
: $t('dataSourceDropdown.noDataSources')
|
||||||
|
}}
|
||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
</Dropdown>
|
</Dropdown>
|
||||||
|
@ -44,30 +50,24 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: null,
|
default: null,
|
||||||
},
|
},
|
||||||
dataSources: {
|
sharedDataSources: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
localDataSources: {
|
||||||
|
type: Array,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
small: {
|
small: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
page: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
pageDataSources() {
|
isOnSharedPage() {
|
||||||
return this.dataSources.filter(
|
return this.localDataSources === null
|
||||||
({ page_id: pageId }) => pageId === this.page.id
|
|
||||||
)
|
|
||||||
},
|
|
||||||
sharedDataSources() {
|
|
||||||
return this.dataSources.filter(
|
|
||||||
({ page_id: pageId }) => pageId !== this.page.id
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
|
|
@ -1,7 +1,7 @@
|
||||||
<template>
|
<template>
|
||||||
<div
|
<div
|
||||||
:key="elementType.name"
|
:key="elementType.name"
|
||||||
v-tooltip="disallowedTypeForAncestry ? disabledElementMessage : null"
|
v-tooltip="disabled ? disabledMessage : null"
|
||||||
class="add-element-card"
|
class="add-element-card"
|
||||||
:class="{ 'add-element-card--disabled': disabled }"
|
:class="{ 'add-element-card--disabled': disabled }"
|
||||||
v-on="$listeners"
|
v-on="$listeners"
|
||||||
|
@ -30,20 +30,15 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
parentType: {
|
|
||||||
type: Object,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
loading: {
|
loading: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
disallowedTypeForAncestry: {
|
disabledMessage: {
|
||||||
type: Boolean,
|
type: String,
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: '',
|
||||||
},
|
},
|
||||||
disabled: {
|
disabled: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
|
@ -51,10 +46,5 @@ export default {
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
|
||||||
disabledElementMessage() {
|
|
||||||
return this.$t('addElementModal.disabledElementTooltip')
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -13,10 +13,9 @@
|
||||||
v-for="elementType in elementTypes"
|
v-for="elementType in elementTypes"
|
||||||
:key="elementType.getType()"
|
:key="elementType.getType()"
|
||||||
:element-type="elementType"
|
:element-type="elementType"
|
||||||
:parent-type="parentElementType"
|
|
||||||
:disallowed-type-for-ancestry="isDisallowedByParent(elementType)"
|
|
||||||
:loading="addingElementType === elementType.getType()"
|
:loading="addingElementType === elementType.getType()"
|
||||||
:disabled="isCardDisabled(elementType)"
|
:disabled="isElementTypeDisabled(elementType)"
|
||||||
|
:disabled-message="getElementTypeDisabledMessage(elementType)"
|
||||||
@click="addElement(elementType)"
|
@click="addElement(elementType)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -29,21 +28,18 @@ import AddElementCard from '@baserow/modules/builder/components/elements/AddElem
|
||||||
import { isSubstringOfStrings } from '@baserow/modules/core/utils/string'
|
import { isSubstringOfStrings } from '@baserow/modules/core/utils/string'
|
||||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||||
import { mapActions } from 'vuex'
|
import { mapActions } from 'vuex'
|
||||||
|
import { PAGE_PLACES } from '../../enums'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'AddElementModal',
|
name: 'AddElementModal',
|
||||||
components: { AddElementCard },
|
components: { AddElementCard },
|
||||||
mixins: [modal],
|
mixins: [modal],
|
||||||
|
inject: ['builder', 'currentPage'],
|
||||||
props: {
|
props: {
|
||||||
page: {
|
page: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
elementTypesAllowed: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: null,
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
@ -51,6 +47,7 @@ export default {
|
||||||
placeInContainer: null,
|
placeInContainer: null,
|
||||||
beforeId: null,
|
beforeId: null,
|
||||||
parentElementId: null,
|
parentElementId: null,
|
||||||
|
pagePlace: null,
|
||||||
addingElementType: null,
|
addingElementType: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -64,57 +61,101 @@ export default {
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
parentElementType() {
|
sharedPage() {
|
||||||
const parentElement = this.$store.getters['element/getElementById'](
|
return this.$store.getters['page/getSharedPage'](this.builder)
|
||||||
this.page,
|
},
|
||||||
this.parentElementId
|
parentElement() {
|
||||||
)
|
if (this.parentElementId) {
|
||||||
return parentElement
|
return this.$store.getters['element/getElementByIdInPages'](
|
||||||
? this.$registry.get('element', parentElement.type)
|
[this.currentPage, this.sharedPage],
|
||||||
: null
|
this.parentElementId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
|
beforeElement() {
|
||||||
|
if (this.beforeId) {
|
||||||
|
return this.$store.getters['element/getElementByIdInPages'](
|
||||||
|
[this.currentPage, this.sharedPage],
|
||||||
|
this.beforeId
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
isDisallowedByParent(elementType) {
|
getElementTypeDisabledMessage(elementType) {
|
||||||
return (
|
if (elementType.getType() === this.addingElementType) {
|
||||||
this.elementTypesAllowed !== null &&
|
// This type is disabled while we add it.
|
||||||
!this.elementTypesAllowed.includes(elementType)
|
return this.$t('addElementModal.elementInProgress')
|
||||||
)
|
}
|
||||||
|
|
||||||
|
return elementType.isDisallowedReason({
|
||||||
|
builder: this.builder,
|
||||||
|
page: this.page,
|
||||||
|
placeInContainer: this.placeInContainer,
|
||||||
|
parentElement: this.parentElement,
|
||||||
|
beforeElement: this.beforeElement,
|
||||||
|
pagePlace: this.pagePlace,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
isCardDisabled(elementType) {
|
isElementTypeDisabled(elementType) {
|
||||||
const isAddingElementType =
|
return !!this.getElementTypeDisabledMessage(elementType)
|
||||||
this.addingElementType !== null &&
|
|
||||||
elementType.getType() === this.addingElementType
|
|
||||||
return isAddingElementType || this.isDisallowedByParent(elementType)
|
|
||||||
},
|
},
|
||||||
...mapActions({
|
...mapActions({
|
||||||
actionCreateElement: 'element/create',
|
actionCreateElement: 'element/create',
|
||||||
}),
|
}),
|
||||||
|
|
||||||
show({ placeInContainer, beforeId, parentElementId } = {}, ...args) {
|
show(
|
||||||
|
{ placeInContainer, beforeId, parentElementId, pagePlace } = {},
|
||||||
|
...args
|
||||||
|
) {
|
||||||
this.placeInContainer = placeInContainer
|
this.placeInContainer = placeInContainer
|
||||||
this.beforeId = beforeId
|
this.beforeId = beforeId
|
||||||
this.parentElementId = parentElementId
|
this.parentElementId = parentElementId
|
||||||
|
this.pagePlace = pagePlace
|
||||||
modal.methods.show.bind(this)(...args)
|
modal.methods.show.bind(this)(...args)
|
||||||
},
|
},
|
||||||
|
|
||||||
async addElement(elementType) {
|
async addElement(elementType) {
|
||||||
if (this.isCardDisabled(elementType)) {
|
if (this.isElementTypeDisabled(elementType)) {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
this.addingElementType = elementType.getType()
|
this.addingElementType = elementType.getType()
|
||||||
const configuration = this.parentElementId
|
|
||||||
? {
|
let beforeId = this.beforeId
|
||||||
parent_element_id: this.parentElementId,
|
let destinationPage
|
||||||
place_in_container: this.placeInContainer,
|
|
||||||
}
|
if (this.parentElementId) {
|
||||||
: null
|
// The page must be the same as the parent one
|
||||||
|
destinationPage =
|
||||||
|
this.parentElement.page_id === this.currentPage.id
|
||||||
|
? this.currentPage
|
||||||
|
: this.sharedPage
|
||||||
|
} else {
|
||||||
|
// The page is forced by the element type page place
|
||||||
|
destinationPage =
|
||||||
|
elementType.getPagePlace() === PAGE_PLACES.CONTENT
|
||||||
|
? this.currentPage
|
||||||
|
: this.sharedPage
|
||||||
|
// If the before element doesn't belong to the same page we must ignore it
|
||||||
|
if (
|
||||||
|
this.beforeElement &&
|
||||||
|
this.beforeElement.page_id !== destinationPage.id
|
||||||
|
) {
|
||||||
|
beforeId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await this.actionCreateElement({
|
await this.actionCreateElement({
|
||||||
page: this.page,
|
page: destinationPage,
|
||||||
elementType: elementType.getType(),
|
elementType: elementType.getType(),
|
||||||
beforeId: this.beforeId,
|
beforeId,
|
||||||
configuration,
|
values: {
|
||||||
|
parent_element_id: this.parentElementId,
|
||||||
|
place_in_container: this.placeInContainer,
|
||||||
|
},
|
||||||
})
|
})
|
||||||
|
|
||||||
this.$emit('element-added')
|
this.$emit('element-added')
|
||||||
|
|
|
@ -1,9 +1,12 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="add-element-zone" @click="!disabled && $emit('add-element')">
|
<div
|
||||||
|
class="add-element-zone"
|
||||||
|
:class="{ 'add-element-zone--disabled': disabled }"
|
||||||
|
>
|
||||||
<div
|
<div
|
||||||
v-tooltip="disabled ? tooltip : null"
|
v-tooltip="disabled ? tooltip : null"
|
||||||
class="add-element-zone__content"
|
class="add-element-zone__button"
|
||||||
:class="{ 'add-element-zone__button--disabled': disabled }"
|
@click="!disabled && $emit('add-element')"
|
||||||
>
|
>
|
||||||
<i class="iconoir-plus add-element-zone__icon"></i>
|
<i class="iconoir-plus add-element-zone__icon"></i>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -20,69 +20,62 @@
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
v-if="isPlacementVisible(PLACEMENTS.LEFT)"
|
v-if="isDirectionVisible(DIRECTIONS.LEFT)"
|
||||||
class="element-preview__menu-item"
|
class="element-preview__menu-item"
|
||||||
:class="{ disabled: isPlacementDisabled(PLACEMENTS.LEFT) }"
|
:class="{
|
||||||
@click="
|
'element-preview__menu-item--disabled': !isAllowedDirection(
|
||||||
!isPlacementDisabled(PLACEMENTS.LEFT) && $emit('move', PLACEMENTS.LEFT)
|
DIRECTIONS.LEFT
|
||||||
"
|
),
|
||||||
|
}"
|
||||||
|
@click="$emit('move', DIRECTIONS.LEFT)"
|
||||||
>
|
>
|
||||||
<i class="iconoir-nav-arrow-left"></i>
|
<i class="iconoir-nav-arrow-left"></i>
|
||||||
<span
|
<span class="element-preview__menu-item-description">
|
||||||
v-if="!isPlacementDisabled(PLACEMENTS.LEFT)"
|
|
||||||
class="element-preview__menu-item-description"
|
|
||||||
>
|
|
||||||
{{ $t('elementMenu.moveLeft') }}
|
{{ $t('elementMenu.moveLeft') }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
v-if="isPlacementVisible(PLACEMENTS.RIGHT)"
|
v-if="isDirectionVisible(DIRECTIONS.RIGHT)"
|
||||||
class="element-preview__menu-item"
|
class="element-preview__menu-item"
|
||||||
:class="{ disabled: isPlacementDisabled(PLACEMENTS.RIGHT) }"
|
:class="{
|
||||||
@click="
|
'element-preview__menu-item--disabled': !isAllowedDirection(
|
||||||
!isPlacementDisabled(PLACEMENTS.RIGHT) &&
|
DIRECTIONS.RIGHT
|
||||||
$emit('move', PLACEMENTS.RIGHT)
|
),
|
||||||
"
|
}"
|
||||||
|
@click="$emit('move', DIRECTIONS.RIGHT)"
|
||||||
>
|
>
|
||||||
<i class="iconoir-nav-arrow-right"></i>
|
<i class="iconoir-nav-arrow-right"></i>
|
||||||
<span
|
<span class="element-preview__menu-item-description">
|
||||||
v-if="!isPlacementDisabled(PLACEMENTS.RIGHT)"
|
|
||||||
class="element-preview__menu-item-description"
|
|
||||||
>
|
|
||||||
{{ $t('elementMenu.moveRight') }}
|
{{ $t('elementMenu.moveRight') }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
v-if="isPlacementVisible(PLACEMENTS.BEFORE)"
|
v-if="isDirectionVisible(DIRECTIONS.BEFORE)"
|
||||||
class="element-preview__menu-item"
|
class="element-preview__menu-item"
|
||||||
:class="{ disabled: isPlacementDisabled(PLACEMENTS.BEFORE) }"
|
:class="{
|
||||||
@click="
|
'element-preview__menu-item--disabled': !isAllowedDirection(
|
||||||
!isPlacementDisabled(PLACEMENTS.BEFORE) &&
|
DIRECTIONS.BEFORE
|
||||||
$emit('move', PLACEMENTS.BEFORE)
|
),
|
||||||
"
|
}"
|
||||||
|
@click="$emit('move', DIRECTIONS.BEFORE)"
|
||||||
>
|
>
|
||||||
<i class="iconoir-nav-arrow-up"></i>
|
<i class="iconoir-nav-arrow-up"></i>
|
||||||
<span
|
<span class="element-preview__menu-item-description">
|
||||||
v-if="!isPlacementDisabled(PLACEMENTS.BEFORE)"
|
|
||||||
class="element-preview__menu-item-description"
|
|
||||||
>
|
|
||||||
{{ $t('elementMenu.moveUp') }}
|
{{ $t('elementMenu.moveUp') }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a
|
||||||
v-if="isPlacementVisible(PLACEMENTS.AFTER)"
|
v-if="isDirectionVisible(DIRECTIONS.AFTER)"
|
||||||
class="element-preview__menu-item"
|
class="element-preview__menu-item"
|
||||||
:class="{ disabled: isPlacementDisabled(PLACEMENTS.AFTER) }"
|
:class="{
|
||||||
@click="
|
'element-preview__menu-item--disabled': !isAllowedDirection(
|
||||||
!isPlacementDisabled(PLACEMENTS.AFTER) &&
|
DIRECTIONS.AFTER
|
||||||
$emit('move', PLACEMENTS.AFTER)
|
),
|
||||||
"
|
}"
|
||||||
|
@click="$emit('move', DIRECTIONS.AFTER)"
|
||||||
>
|
>
|
||||||
<i class="iconoir-nav-arrow-down"></i>
|
<i class="iconoir-nav-arrow-down"></i>
|
||||||
<span
|
<span class="element-preview__menu-item-description">
|
||||||
v-if="!isPlacementDisabled(PLACEMENTS.AFTER)"
|
|
||||||
class="element-preview__menu-item-description"
|
|
||||||
>
|
|
||||||
{{ $t('elementMenu.moveDown') }}
|
{{ $t('elementMenu.moveDown') }}
|
||||||
</span>
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
@ -96,7 +89,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { PLACEMENTS } from '@baserow/modules/builder/enums'
|
import { DIRECTIONS } from '@baserow/modules/builder/enums'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ElementMenu',
|
name: 'ElementMenu',
|
||||||
|
@ -111,26 +104,26 @@ export default {
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
placements: {
|
directions: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => [PLACEMENTS.BEFORE, PLACEMENTS.AFTER],
|
default: () => [DIRECTIONS.BEFORE, DIRECTIONS.AFTER],
|
||||||
},
|
},
|
||||||
placementsDisabled: {
|
allowedDirections: {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: false,
|
required: false,
|
||||||
default: () => [],
|
default: () => [],
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
PLACEMENTS: () => PLACEMENTS,
|
DIRECTIONS: () => DIRECTIONS,
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
isPlacementVisible(placement) {
|
isDirectionVisible(direction) {
|
||||||
return this.placements.includes(placement)
|
return this.directions.includes(direction)
|
||||||
},
|
},
|
||||||
isPlacementDisabled(placement) {
|
isAllowedDirection(direction) {
|
||||||
return this.placementsDisabled.includes(placement)
|
return this.allowedDirections.includes(direction)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
|
@ -20,38 +20,37 @@
|
||||||
v-show="isSelected"
|
v-show="isSelected"
|
||||||
v-if="canCreate"
|
v-if="canCreate"
|
||||||
class="element-preview__insert element-preview__insert--top"
|
class="element-preview__insert element-preview__insert--top"
|
||||||
@click="showAddElementModal(PLACEMENTS.BEFORE)"
|
@click="showAddElementModal(DIRECTIONS.BEFORE)"
|
||||||
/>
|
/>
|
||||||
<ElementMenu
|
<ElementMenu
|
||||||
v-if="isSelected && canUpdate"
|
v-if="isSelected && canUpdate"
|
||||||
:placements="placements"
|
:directions="directions"
|
||||||
:placements-disabled="placementsDisabled"
|
:allowed-directions="allowedMoveDirections"
|
||||||
:is-duplicating="isDuplicating"
|
:is-duplicating="isDuplicating"
|
||||||
:has-parent="!!parentElement"
|
:has-parent="!!parentElement"
|
||||||
@delete="deleteElement"
|
@delete="deleteElement"
|
||||||
@move="$emit('move', $event)"
|
@move="onMove"
|
||||||
@duplicate="duplicateElement"
|
@duplicate="duplicateElement"
|
||||||
@select-parent="selectParentElement()"
|
@select-parent="selectParentElement()"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<PageElement
|
<PageElement
|
||||||
:element="element"
|
:element="element"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
class="element--read-only"
|
class="element--read-only"
|
||||||
:application-context-additions="applicationContextAdditions"
|
:application-context-additions="applicationContextAdditions"
|
||||||
|
v-on="$listeners"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<InsertElementButton
|
<InsertElementButton
|
||||||
v-show="isSelected"
|
v-show="isSelected"
|
||||||
v-if="canCreate"
|
v-if="canCreate"
|
||||||
class="element-preview__insert element-preview__insert--bottom"
|
class="element-preview__insert element-preview__insert--bottom"
|
||||||
@click="showAddElementModal(PLACEMENTS.AFTER)"
|
@click="showAddElementModal(DIRECTIONS.AFTER)"
|
||||||
/>
|
/>
|
||||||
<AddElementModal
|
<AddElementModal
|
||||||
v-if="canCreate"
|
v-if="canCreate"
|
||||||
ref="addElementModal"
|
ref="addElementModal"
|
||||||
:element-types-allowed="elementTypesAllowed"
|
:page="elementPage"
|
||||||
:page="page"
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<i
|
<i
|
||||||
|
@ -65,7 +64,7 @@
|
||||||
import ElementMenu from '@baserow/modules/builder/components/elements/ElementMenu'
|
import ElementMenu from '@baserow/modules/builder/components/elements/ElementMenu'
|
||||||
import InsertElementButton from '@baserow/modules/builder/components/elements/InsertElementButton'
|
import InsertElementButton from '@baserow/modules/builder/components/elements/InsertElementButton'
|
||||||
import PageElement from '@baserow/modules/builder/components/page/PageElement'
|
import PageElement from '@baserow/modules/builder/components/page/PageElement'
|
||||||
import { PLACEMENTS } from '@baserow/modules/builder/enums'
|
import { DIRECTIONS } from '@baserow/modules/builder/enums'
|
||||||
import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal'
|
import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal'
|
||||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||||
import { mapActions, mapGetters } from 'vuex'
|
import { mapActions, mapGetters } from 'vuex'
|
||||||
|
@ -85,27 +84,17 @@ export default {
|
||||||
InsertElementButton,
|
InsertElementButton,
|
||||||
PageElement,
|
PageElement,
|
||||||
},
|
},
|
||||||
inject: ['workspace', 'builder', 'page', 'mode'],
|
inject: ['workspace', 'builder', 'mode', 'currentPage'],
|
||||||
props: {
|
props: {
|
||||||
element: {
|
element: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
isLastElement: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
isFirstElement: {
|
isFirstElement: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
default: false,
|
default: false,
|
||||||
},
|
},
|
||||||
isRootElement: {
|
|
||||||
type: Boolean,
|
|
||||||
required: false,
|
|
||||||
default: false,
|
|
||||||
},
|
|
||||||
applicationContextAdditions: {
|
applicationContextAdditions: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: false,
|
required: false,
|
||||||
|
@ -124,7 +113,23 @@ export default {
|
||||||
getClosestSiblingElement: 'element/getClosestSiblingElement',
|
getClosestSiblingElement: 'element/getClosestSiblingElement',
|
||||||
loggedUser: 'userSourceUser/getUser',
|
loggedUser: 'userSourceUser/getUser',
|
||||||
}),
|
}),
|
||||||
|
elementPage() {
|
||||||
|
// We use the page from the element itself
|
||||||
|
return this.$store.getters['page/getById'](
|
||||||
|
this.builder,
|
||||||
|
this.element.page_id
|
||||||
|
)
|
||||||
|
},
|
||||||
isVisible() {
|
isVisible() {
|
||||||
|
if (
|
||||||
|
!this.elementType.isVisible({
|
||||||
|
element: this.element,
|
||||||
|
currentPage: this.currentPage,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const isAuthenticated = this.$store.getters[
|
const isAuthenticated = this.$store.getters[
|
||||||
'userSourceUser/isAuthenticated'
|
'userSourceUser/isAuthenticated'
|
||||||
](this.builder)
|
](this.builder)
|
||||||
|
@ -151,13 +156,13 @@ export default {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
PLACEMENTS: () => PLACEMENTS,
|
DIRECTIONS: () => DIRECTIONS,
|
||||||
placements() {
|
directions() {
|
||||||
return [
|
return [
|
||||||
PLACEMENTS.BEFORE,
|
DIRECTIONS.BEFORE,
|
||||||
PLACEMENTS.AFTER,
|
DIRECTIONS.AFTER,
|
||||||
PLACEMENTS.LEFT,
|
DIRECTIONS.LEFT,
|
||||||
PLACEMENTS.RIGHT,
|
DIRECTIONS.RIGHT,
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
parentOfElementSelected() {
|
parentOfElementSelected() {
|
||||||
|
@ -165,24 +170,34 @@ export default {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return this.$store.getters['element/getElementById'](
|
return this.$store.getters['element/getElementById'](
|
||||||
this.page,
|
this.elementPage,
|
||||||
this.elementSelected.parent_element_id
|
this.elementSelected.parent_element_id
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
placementsDisabled() {
|
elementsAround() {
|
||||||
const elementType = this.$registry.get('element', this.element.type)
|
return this.elementType.getElementsAround({
|
||||||
return elementType.getPlacementsDisabled(this.page, this.element)
|
builder: this.builder,
|
||||||
|
page: this.currentPage,
|
||||||
|
withSharedPage: true,
|
||||||
|
element: this.element,
|
||||||
|
})
|
||||||
},
|
},
|
||||||
elementTypesAllowed() {
|
nextPlaces() {
|
||||||
return (
|
return this.elementType.getNextPlaces({
|
||||||
this.parentElementType?.childElementTypes(this.page, this.element) ||
|
builder: this.builder,
|
||||||
null
|
page: this.elementPage,
|
||||||
)
|
element: this.element,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
allowedMoveDirections() {
|
||||||
|
return Object.entries(this.nextPlaces)
|
||||||
|
.filter(([, nextPlace]) => !!nextPlace)
|
||||||
|
.map(([direction]) => direction)
|
||||||
},
|
},
|
||||||
canCreate() {
|
canCreate() {
|
||||||
return this.$hasPermission(
|
return this.$hasPermission(
|
||||||
'builder.page.create_element',
|
'builder.page.create_element',
|
||||||
this.page,
|
this.currentPage,
|
||||||
this.workspace.id
|
this.workspace.id
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -200,7 +215,7 @@ export default {
|
||||||
if (!this.elementSelected) {
|
if (!this.elementSelected) {
|
||||||
return []
|
return []
|
||||||
}
|
}
|
||||||
return this.elementAncestors(this.page, this.elementSelected).map(
|
return this.elementAncestors(this.elementPage, this.elementSelected).map(
|
||||||
({ id }) => id
|
({ id }) => id
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -215,7 +230,7 @@ export default {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return this.$store.getters['element/getElementById'](
|
return this.$store.getters['element/getElementById'](
|
||||||
this.page,
|
this.elementPage,
|
||||||
this.element.parent_element_id
|
this.element.parent_element_id
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -224,15 +239,9 @@ export default {
|
||||||
? this.$registry.get('element', this.parentElement?.type)
|
? this.$registry.get('element', this.parentElement?.type)
|
||||||
: null
|
: null
|
||||||
},
|
},
|
||||||
nextElement() {
|
|
||||||
return this.$store.getters['element/getNextElement'](
|
|
||||||
this.page,
|
|
||||||
this.element
|
|
||||||
)
|
|
||||||
},
|
|
||||||
inError() {
|
inError() {
|
||||||
return this.elementType.isInError({
|
return this.elementType.isInError({
|
||||||
page: this.page,
|
page: this.elementPage,
|
||||||
element: this.element,
|
element: this.element,
|
||||||
builder: this.builder,
|
builder: this.builder,
|
||||||
})
|
})
|
||||||
|
@ -284,6 +293,9 @@ export default {
|
||||||
actionDeleteElement: 'element/delete',
|
actionDeleteElement: 'element/delete',
|
||||||
actionSelectElement: 'element/select',
|
actionSelectElement: 'element/select',
|
||||||
}),
|
}),
|
||||||
|
onMove(direction) {
|
||||||
|
this.$emit('move', { element: this.element, direction })
|
||||||
|
},
|
||||||
onSelect($event) {
|
onSelect($event) {
|
||||||
// Here we check that the event has been emitted for this particular element
|
// Here we check that the event has been emitted for this particular element
|
||||||
// If we found an intermediate DOM element with the class `element-preview`,
|
// If we found an intermediate DOM element with the class `element-preview`,
|
||||||
|
@ -300,23 +312,32 @@ export default {
|
||||||
this.actionSelectElement({ element: this.element })
|
this.actionSelectElement({ element: this.element })
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
showAddElementModal(placement) {
|
showAddElementModal(direction) {
|
||||||
|
const rootElement = this.$store.getters['element/getAncestors'](
|
||||||
|
this.elementPage,
|
||||||
|
this.element,
|
||||||
|
{ includeSelf: true }
|
||||||
|
)[0]
|
||||||
|
const rootElementType = this.$registry.get('element', rootElement.type)
|
||||||
|
const pagePlace = rootElementType.getPagePlace()
|
||||||
|
|
||||||
this.$refs.addElementModal.show({
|
this.$refs.addElementModal.show({
|
||||||
placeInContainer: this.element.place_in_container,
|
placeInContainer: this.element.place_in_container,
|
||||||
parentElementId: this.element.parent_element_id,
|
parentElementId: this.element.parent_element_id,
|
||||||
beforeId: this.getBeforeId(placement),
|
beforeId: this.getBeforeId(direction),
|
||||||
|
pagePlace,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
getBeforeId(placement) {
|
getBeforeId(direction) {
|
||||||
return placement === PLACEMENTS.BEFORE
|
return direction === DIRECTIONS.BEFORE
|
||||||
? this.element.id
|
? this.element.id
|
||||||
: this.nextElement?.id || null
|
: this.elementsAround[DIRECTIONS.AFTER]?.id || null
|
||||||
},
|
},
|
||||||
async duplicateElement() {
|
async duplicateElement() {
|
||||||
this.isDuplicating = true
|
this.isDuplicating = true
|
||||||
try {
|
try {
|
||||||
await this.actionDuplicateElement({
|
await this.actionDuplicateElement({
|
||||||
page: this.page,
|
page: this.elementPage,
|
||||||
elementId: this.element.id,
|
elementId: this.element.id,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -326,12 +347,15 @@ export default {
|
||||||
},
|
},
|
||||||
async deleteElement() {
|
async deleteElement() {
|
||||||
try {
|
try {
|
||||||
const siblingElementToSelect = this.getClosestSiblingElement(
|
const siblingElementToSelect =
|
||||||
this.page,
|
this.elementsAround[DIRECTIONS.AFTER] ||
|
||||||
this.elementSelected
|
this.elementsAround[DIRECTIONS.BEFORE] ||
|
||||||
)
|
this.elementsAround[DIRECTIONS.LEFT] ||
|
||||||
|
this.elementsAround[DIRECTIONS.RIGHT] ||
|
||||||
|
this.parentOfElementSelected
|
||||||
|
|
||||||
await this.actionDeleteElement({
|
await this.actionDeleteElement({
|
||||||
page: this.page,
|
page: this.elementPage,
|
||||||
elementId: this.element.id,
|
elementId: this.element.id,
|
||||||
})
|
})
|
||||||
if (siblingElementToSelect?.id) {
|
if (siblingElementToSelect?.id) {
|
||||||
|
|
|
@ -1,8 +1,5 @@
|
||||||
<template>
|
<template>
|
||||||
<ul
|
<ul class="elements-list">
|
||||||
v-auto-overflow-scroll
|
|
||||||
class="elements-list__items elements-list__items--no-max-height"
|
|
||||||
>
|
|
||||||
<ElementsListItem
|
<ElementsListItem
|
||||||
v-for="element in filteredElements"
|
v-for="element in filteredElements"
|
||||||
:key="element.id"
|
:key="element.id"
|
||||||
|
@ -19,7 +16,6 @@ import ElementsListItem from '@baserow/modules/builder/components/elements/Eleme
|
||||||
export default {
|
export default {
|
||||||
name: 'ElementsList',
|
name: 'ElementsList',
|
||||||
components: { ElementsListItem },
|
components: { ElementsListItem },
|
||||||
inject: ['page'],
|
|
||||||
props: {
|
props: {
|
||||||
elements: {
|
elements: {
|
||||||
type: Array,
|
type: Array,
|
||||||
|
|
|
@ -1,15 +1,15 @@
|
||||||
<template>
|
<template>
|
||||||
<li :key="element.id" class="elements-list__item">
|
<li
|
||||||
<a
|
:key="element.id"
|
||||||
class="elements-list__item-link"
|
class="elements-list-item"
|
||||||
:class="{
|
:class="{
|
||||||
'elements-list__item-link--selected': element.id === elementSelectedId,
|
'elements-list-item--selected': element.id === elementSelectedId,
|
||||||
}"
|
}"
|
||||||
@click="$emit('select', element)"
|
>
|
||||||
>
|
<a class="elements-list-item__link" @click="$emit('select', element)">
|
||||||
<span class="elements-list__item-name">
|
<span class="elements-list-item__name">
|
||||||
<i :class="`${elementType.iconClass} elements-list__item-icon`"></i>
|
<i :class="`${elementType.iconClass} elements-list-item__icon`"></i>
|
||||||
<span class="elements-list__item-name-text">{{
|
<span class="elements-list-item__name-text">{{
|
||||||
elementType.getDisplayName(element, applicationContext)
|
elementType.getDisplayName(element, applicationContext)
|
||||||
}}</span>
|
}}</span>
|
||||||
</span>
|
</span>
|
||||||
|
@ -33,7 +33,7 @@ export default {
|
||||||
ElementsList: () =>
|
ElementsList: () =>
|
||||||
import('@baserow/modules/builder/components/elements/ElementsList'),
|
import('@baserow/modules/builder/components/elements/ElementsList'),
|
||||||
},
|
},
|
||||||
inject: ['builder', 'page', 'mode'],
|
inject: ['builder', 'mode'],
|
||||||
props: {
|
props: {
|
||||||
element: {
|
element: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -55,8 +55,18 @@ export default {
|
||||||
elementType() {
|
elementType() {
|
||||||
return this.$registry.get('element', this.element.type)
|
return this.$registry.get('element', this.element.type)
|
||||||
},
|
},
|
||||||
|
elementPage() {
|
||||||
|
// We use the page from the element itself
|
||||||
|
return this.$store.getters['page/getById'](
|
||||||
|
this.builder,
|
||||||
|
this.element.page_id
|
||||||
|
)
|
||||||
|
},
|
||||||
children() {
|
children() {
|
||||||
return this.$store.getters['element/getChildren'](this.page, this.element)
|
return this.$store.getters['element/getChildren'](
|
||||||
|
this.elementPage,
|
||||||
|
this.element
|
||||||
|
)
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Responsible for returning elements to display in `ElementsList`.
|
* Responsible for returning elements to display in `ElementsList`.
|
||||||
|
@ -76,7 +86,7 @@ export default {
|
||||||
applicationContext() {
|
applicationContext() {
|
||||||
return {
|
return {
|
||||||
builder: this.builder,
|
builder: this.builder,
|
||||||
page: this.page,
|
page: this.elementPage,
|
||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
element: this.element,
|
element: this.element,
|
||||||
}
|
}
|
||||||
|
|
|
@ -75,7 +75,9 @@ export default {
|
||||||
}
|
}
|
||||||
if (this.target === 'self' && this.url.startsWith('/')) {
|
if (this.target === 'self' && this.url.startsWith('/')) {
|
||||||
event.preventDefault()
|
event.preventDefault()
|
||||||
this.$router.push(this.url)
|
if (this.$route.path !== this.url) {
|
||||||
|
this.$router.push(this.url)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|
|
@ -25,8 +25,13 @@
|
||||||
</slot>
|
</slot>
|
||||||
</template>
|
</template>
|
||||||
<template #empty-state>
|
<template #empty-state>
|
||||||
<div class="ab-table__empty-message">
|
<div class="ab-table__empty-state">
|
||||||
{{ emptyStateMessage }}
|
<template v-if="contentLoading">
|
||||||
|
<div class="loading-spinner" />
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
{{ $t('abTable.empty') }}
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
</BaserowTable>
|
</BaserowTable>
|
||||||
|
|
|
@ -20,7 +20,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
inject: ['builder', 'page'],
|
inject: ['builder', 'currentPage'],
|
||||||
props: {
|
props: {
|
||||||
element: {
|
element: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
@ -33,7 +33,7 @@ export default {
|
||||||
},
|
},
|
||||||
dataSource() {
|
dataSource() {
|
||||||
return this.$store.getters['dataSource/getPagesDataSourceById'](
|
return this.$store.getters['dataSource/getPagesDataSourceById'](
|
||||||
[this.page, this.sharedPage],
|
[this.currentPage, this.sharedPage],
|
||||||
this.element.data_source_id
|
this.element.data_source_id
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -22,7 +22,7 @@
|
||||||
v-if="mode === 'editing'"
|
v-if="mode === 'editing'"
|
||||||
:element="childCurrent"
|
:element="childCurrent"
|
||||||
:application-context-additions="applicationContextAdditions"
|
:application-context-additions="applicationContextAdditions"
|
||||||
@move="move(childCurrent, $event)"
|
@move="$emit('move', $event)"
|
||||||
></ElementPreview>
|
></ElementPreview>
|
||||||
<PageElement
|
<PageElement
|
||||||
v-else
|
v-else
|
||||||
|
@ -35,21 +35,21 @@
|
||||||
<AddElementZone
|
<AddElementZone
|
||||||
v-else-if="
|
v-else-if="
|
||||||
mode === 'editing' &&
|
mode === 'editing' &&
|
||||||
$hasPermission('builder.page.create_element', page, workspace.id)
|
$hasPermission(
|
||||||
|
'builder.page.create_element',
|
||||||
|
elementPage,
|
||||||
|
workspace.id
|
||||||
|
)
|
||||||
"
|
"
|
||||||
|
:page="elementPage"
|
||||||
@add-element="showAddElementModal(columnIndex)"
|
@add-element="showAddElementModal(columnIndex)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<AddElementModal
|
<AddElementModal ref="addElementModal" :page="elementPage" />
|
||||||
ref="addElementModal"
|
|
||||||
:page="page"
|
|
||||||
:element-types-allowed="elementType.childElementTypes(page, element)"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions } from 'vuex'
|
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
|
||||||
import AddElementZone from '@baserow/modules/builder/components/elements/AddElementZone'
|
import AddElementZone from '@baserow/modules/builder/components/elements/AddElementZone'
|
||||||
|
@ -58,7 +58,6 @@ import containerElement from '@baserow/modules/builder/mixins/containerElement'
|
||||||
import PageElement from '@baserow/modules/builder/components/page/PageElement'
|
import PageElement from '@baserow/modules/builder/components/page/PageElement'
|
||||||
import ElementPreview from '@baserow/modules/builder/components/elements/ElementPreview'
|
import ElementPreview from '@baserow/modules/builder/components/elements/ElementPreview'
|
||||||
import { VERTICAL_ALIGNMENTS } from '@baserow/modules/builder/enums'
|
import { VERTICAL_ALIGNMENTS } from '@baserow/modules/builder/enums'
|
||||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
|
||||||
import { dimensionMixin } from '@baserow/modules/core/mixins/dimensions'
|
import { dimensionMixin } from '@baserow/modules/core/mixins/dimensions'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
|
@ -136,26 +135,12 @@ export default {
|
||||||
this.dimensions.targetElement = this.$el.parentElement
|
this.dimensions.targetElement = this.$el.parentElement
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions({
|
|
||||||
actionMoveElement: 'element/moveElement',
|
|
||||||
}),
|
|
||||||
showAddElementModal(columnIndex) {
|
showAddElementModal(columnIndex) {
|
||||||
this.$refs.addElementModal.show({
|
this.$refs.addElementModal.show({
|
||||||
placeInContainer: `${columnIndex}`,
|
placeInContainer: `${columnIndex}`,
|
||||||
parentElementId: this.element.id,
|
parentElementId: this.element.id,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async move(element, placement) {
|
|
||||||
try {
|
|
||||||
await this.actionMoveElement({
|
|
||||||
page: this.page,
|
|
||||||
element,
|
|
||||||
placement,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
notifyIf(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -4,14 +4,13 @@
|
||||||
v-if="
|
v-if="
|
||||||
mode === 'editing' &&
|
mode === 'editing' &&
|
||||||
children.length === 0 &&
|
children.length === 0 &&
|
||||||
$hasPermission('builder.page.create_element', page, workspace.id)
|
$hasPermission('builder.page.create_element', currentPage, workspace.id)
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
<AddElementZone @add-element="showAddElementModal"></AddElementZone>
|
<AddElementZone @add-element="showAddElementModal"></AddElementZone>
|
||||||
<AddElementModal
|
<AddElementModal
|
||||||
ref="addElementModal"
|
ref="addElementModal"
|
||||||
:page="page"
|
:page="elementPage"
|
||||||
:element-types-allowed="elementType.childElementTypes(page, element)"
|
|
||||||
></AddElementModal>
|
></AddElementModal>
|
||||||
</div>
|
</div>
|
||||||
<div v-else>
|
<div v-else>
|
||||||
|
@ -20,7 +19,7 @@
|
||||||
v-if="mode === 'editing'"
|
v-if="mode === 'editing'"
|
||||||
:key="child.id"
|
:key="child.id"
|
||||||
:element="child"
|
:element="child"
|
||||||
@move="moveElement(child, $event)"
|
@move="$emit('move', $event)"
|
||||||
/>
|
/>
|
||||||
<PageElement
|
<PageElement
|
||||||
v-else
|
v-else
|
||||||
|
@ -42,15 +41,12 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions } from 'vuex'
|
|
||||||
|
|
||||||
import AddElementZone from '@baserow/modules/builder/components/elements/AddElementZone.vue'
|
import AddElementZone from '@baserow/modules/builder/components/elements/AddElementZone.vue'
|
||||||
import containerElement from '@baserow/modules/builder/mixins/containerElement'
|
import containerElement from '@baserow/modules/builder/mixins/containerElement'
|
||||||
import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal.vue'
|
import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal.vue'
|
||||||
import ElementPreview from '@baserow/modules/builder/components/elements/ElementPreview.vue'
|
import ElementPreview from '@baserow/modules/builder/components/elements/ElementPreview.vue'
|
||||||
import PageElement from '@baserow/modules/builder/components/page/PageElement.vue'
|
import PageElement from '@baserow/modules/builder/components/page/PageElement.vue'
|
||||||
import { ensureString } from '@baserow/modules/core/utils/validator'
|
import { ensureString } from '@baserow/modules/core/utils/validator'
|
||||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'FormContainerElement',
|
name: 'FormContainerElement',
|
||||||
|
@ -64,7 +60,6 @@ export default {
|
||||||
props: {
|
props: {
|
||||||
/**
|
/**
|
||||||
* @type {Object}
|
* @type {Object}
|
||||||
* @property button_color - The submit button's color.
|
|
||||||
* @property submit_button_label - The label of the submit button
|
* @property submit_button_label - The label of the submit button
|
||||||
* @property reset_initial_values_post_submission - Whether to reset the form
|
* @property reset_initial_values_post_submission - Whether to reset the form
|
||||||
* elements to their initial value or not, following a successful submission.
|
* elements to their initial value or not, following a successful submission.
|
||||||
|
@ -83,7 +78,7 @@ export default {
|
||||||
},
|
},
|
||||||
getFormElementDescendants() {
|
getFormElementDescendants() {
|
||||||
const descendants = this.$store.getters['element/getDescendants'](
|
const descendants = this.$store.getters['element/getDescendants'](
|
||||||
this.page,
|
this.elementPage,
|
||||||
this.element
|
this.element
|
||||||
)
|
)
|
||||||
return descendants
|
return descendants
|
||||||
|
@ -107,7 +102,7 @@ export default {
|
||||||
recordIndexPath
|
recordIndexPath
|
||||||
)
|
)
|
||||||
return this.$store.getters['formData/getElementInvalid'](
|
return this.$store.getters['formData/getElementInvalid'](
|
||||||
this.page,
|
this.elementPage,
|
||||||
uniqueElementId
|
uniqueElementId
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
@ -115,9 +110,6 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions({
|
|
||||||
actionMoveElement: 'element/moveElement',
|
|
||||||
}),
|
|
||||||
/*
|
/*
|
||||||
* Responsible for marking all form element descendents in this form container
|
* Responsible for marking all form element descendents in this form container
|
||||||
* as touched, or not touched, depending on what we're achieving in validation.
|
* as touched, or not touched, depending on what we're achieving in validation.
|
||||||
|
@ -131,7 +123,7 @@ export default {
|
||||||
recordIndexPath
|
recordIndexPath
|
||||||
)
|
)
|
||||||
this.$store.dispatch('formData/setElementTouched', {
|
this.$store.dispatch('formData/setElementTouched', {
|
||||||
page: this.page,
|
page: this.elementPage,
|
||||||
wasTouched,
|
wasTouched,
|
||||||
uniqueElementId,
|
uniqueElementId,
|
||||||
})
|
})
|
||||||
|
@ -169,7 +161,7 @@ export default {
|
||||||
),
|
),
|
||||||
}
|
}
|
||||||
this.$store.dispatch('formData/setFormData', {
|
this.$store.dispatch('formData/setFormData', {
|
||||||
page: this.page,
|
page: this.elementPage,
|
||||||
payload,
|
payload,
|
||||||
uniqueElementId,
|
uniqueElementId,
|
||||||
})
|
})
|
||||||
|
@ -197,17 +189,6 @@ export default {
|
||||||
parentElementId: this.element.id,
|
parentElementId: this.element.id,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async moveElement(element, placement) {
|
|
||||||
try {
|
|
||||||
await this.actionMoveElement({
|
|
||||||
page: this.page,
|
|
||||||
element,
|
|
||||||
placement,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
notifyIf(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -0,0 +1,82 @@
|
||||||
|
<template>
|
||||||
|
<div>
|
||||||
|
<template
|
||||||
|
v-if="
|
||||||
|
mode === 'editing' &&
|
||||||
|
children.length === 0 &&
|
||||||
|
$hasPermission('builder.page.create_element', currentPage, workspace.id)
|
||||||
|
"
|
||||||
|
>
|
||||||
|
<AddElementZone @add-element="showAddElementModal"></AddElementZone>
|
||||||
|
<AddElementModal
|
||||||
|
ref="addElementModal"
|
||||||
|
:page="elementPage"
|
||||||
|
></AddElementModal>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<template v-for="child in children">
|
||||||
|
<ElementPreview
|
||||||
|
v-if="mode === 'editing'"
|
||||||
|
:key="child.id"
|
||||||
|
:element="child"
|
||||||
|
@move="$emit('move', $event)"
|
||||||
|
/>
|
||||||
|
<PageElement
|
||||||
|
v-else
|
||||||
|
:key="`${child.id}else`"
|
||||||
|
:element="child"
|
||||||
|
:mode="mode"
|
||||||
|
/>
|
||||||
|
</template>
|
||||||
|
</template>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import AddElementZone from '@baserow/modules/builder/components/elements/AddElementZone.vue'
|
||||||
|
import containerElement from '@baserow/modules/builder/mixins/containerElement'
|
||||||
|
import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal.vue'
|
||||||
|
import ElementPreview from '@baserow/modules/builder/components/elements/ElementPreview.vue'
|
||||||
|
import PageElement from '@baserow/modules/builder/components/page/PageElement.vue'
|
||||||
|
import { ensureString } from '@baserow/modules/core/utils/validator'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MultiPageContainerElement',
|
||||||
|
components: {
|
||||||
|
PageElement,
|
||||||
|
ElementPreview,
|
||||||
|
AddElementModal,
|
||||||
|
AddElementZone,
|
||||||
|
},
|
||||||
|
mixins: [containerElement],
|
||||||
|
props: {
|
||||||
|
/**
|
||||||
|
* @type {Object}
|
||||||
|
* @property page_position - [header|footer|left|right]
|
||||||
|
* Position of this element on the page.
|
||||||
|
* @property behaviour - [scroll|fixed|sticky]
|
||||||
|
* How this element follow the scroll of the page.
|
||||||
|
* @property shared_type - [all_pages|only_pages|except_pages] Type of share
|
||||||
|
* @property pages - List of pages the element is visible or excluded depending on
|
||||||
|
* the share_type.
|
||||||
|
*/
|
||||||
|
element: {
|
||||||
|
type: Object,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
submitButtonLabelResolved() {
|
||||||
|
return ensureString(this.resolveFormula(this.element.submit_button_label))
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
showAddElementModal() {
|
||||||
|
this.$refs.addElementModal.show({
|
||||||
|
placeInContainer: null,
|
||||||
|
parentElementId: this.element.id,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -34,7 +34,7 @@
|
||||||
index,
|
index,
|
||||||
],
|
],
|
||||||
}"
|
}"
|
||||||
@move="moveElement(child, $event)"
|
@move="$emit('move', $event)"
|
||||||
/>
|
/>
|
||||||
<!-- Other iterations are not editable -->
|
<!-- Other iterations are not editable -->
|
||||||
<!-- Override the mode so that any children are in public mode -->
|
<!-- Override the mode so that any children are in public mode -->
|
||||||
|
@ -68,10 +68,7 @@
|
||||||
></AddElementZone>
|
></AddElementZone>
|
||||||
<AddElementModal
|
<AddElementModal
|
||||||
ref="addElementModal"
|
ref="addElementModal"
|
||||||
:page="page"
|
:page="elementPage"
|
||||||
:element-types-allowed="
|
|
||||||
elementType.childElementTypes(page, element)
|
|
||||||
"
|
|
||||||
></AddElementModal>
|
></AddElementModal>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
@ -92,10 +89,7 @@
|
||||||
></AddElementZone>
|
></AddElementZone>
|
||||||
<AddElementModal
|
<AddElementModal
|
||||||
ref="addElementModal"
|
ref="addElementModal"
|
||||||
:page="page"
|
:page="elementPage"
|
||||||
:element-types-allowed="
|
|
||||||
elementType.childElementTypes(page, element)
|
|
||||||
"
|
|
||||||
></AddElementModal>
|
></AddElementModal>
|
||||||
</template>
|
</template>
|
||||||
<!-- We have no contents, but we do have children in edit mode -->
|
<!-- We have no contents, but we do have children in edit mode -->
|
||||||
|
@ -106,7 +100,7 @@
|
||||||
v-for="child in children"
|
v-for="child in children"
|
||||||
:key="child.id"
|
:key="child.id"
|
||||||
:element="child"
|
:element="child"
|
||||||
@move="moveElement(child, $event)"
|
@move="$emit('move', $event)"
|
||||||
/>
|
/>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</template>
|
||||||
|
@ -127,7 +121,7 @@
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import { mapActions, mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
|
|
||||||
import AddElementZone from '@baserow/modules/builder/components/elements/AddElementZone'
|
import AddElementZone from '@baserow/modules/builder/components/elements/AddElementZone'
|
||||||
import containerElement from '@baserow/modules/builder/mixins/containerElement'
|
import containerElement from '@baserow/modules/builder/mixins/containerElement'
|
||||||
|
@ -135,7 +129,6 @@ import collectionElement from '@baserow/modules/builder/mixins/collectionElement
|
||||||
import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal'
|
import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal'
|
||||||
import ElementPreview from '@baserow/modules/builder/components/elements/ElementPreview'
|
import ElementPreview from '@baserow/modules/builder/components/elements/ElementPreview'
|
||||||
import PageElement from '@baserow/modules/builder/components/page/PageElement'
|
import PageElement from '@baserow/modules/builder/components/page/PageElement'
|
||||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
|
||||||
import { ensureString } from '@baserow/modules/core/utils/validator'
|
import { ensureString } from '@baserow/modules/core/utils/validator'
|
||||||
import { RepeatElementType } from '@baserow/modules/builder/elementTypes'
|
import { RepeatElementType } from '@baserow/modules/builder/elementTypes'
|
||||||
import CollectionElementHeader from '@baserow/modules/builder/components/elements/components/CollectionElementHeader'
|
import CollectionElementHeader from '@baserow/modules/builder/components/elements/components/CollectionElementHeader'
|
||||||
|
@ -173,7 +166,7 @@ export default {
|
||||||
},
|
},
|
||||||
repeatElementIsNested() {
|
repeatElementIsNested() {
|
||||||
return this.elementType.hasAncestorOfType(
|
return this.elementType.hasAncestorOfType(
|
||||||
this.page,
|
this.elementPage,
|
||||||
this.element,
|
this.element,
|
||||||
RepeatElementType.getType()
|
RepeatElementType.getType()
|
||||||
)
|
)
|
||||||
|
@ -212,26 +205,12 @@ export default {
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
...mapActions({
|
|
||||||
actionMoveElement: 'element/moveElement',
|
|
||||||
}),
|
|
||||||
showAddElementModal() {
|
showAddElementModal() {
|
||||||
this.$refs.addElementModal.show({
|
this.$refs.addElementModal.show({
|
||||||
placeInContainer: null,
|
placeInContainer: null,
|
||||||
parentElementId: this.element.id,
|
parentElementId: this.element.id,
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
async moveElement(element, placement) {
|
|
||||||
try {
|
|
||||||
await this.actionMoveElement({
|
|
||||||
page: this.page,
|
|
||||||
element,
|
|
||||||
placement,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
notifyIf(error)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
|
@ -31,7 +31,7 @@ export default {
|
||||||
)
|
)
|
||||||
const workflowActions = this.$store.getters[
|
const workflowActions = this.$store.getters[
|
||||||
'workflowAction/getElementWorkflowActions'
|
'workflowAction/getElementWorkflowActions'
|
||||||
](this.page, this.element.id)
|
](this.elementPage, this.element.id)
|
||||||
return workflowActions
|
return workflowActions
|
||||||
.filter((wa) => wa.event === this.eventName)
|
.filter((wa) => wa.event === this.eventName)
|
||||||
.some((workflowAction) =>
|
.some((workflowAction) =>
|
||||||
|
|
|
@ -31,7 +31,7 @@
|
||||||
|
|
||||||
<div
|
<div
|
||||||
v-if="allowAllRolesExceptSelected || disallowAllRolesExceptSelected"
|
v-if="allowAllRolesExceptSelected || disallowAllRolesExceptSelected"
|
||||||
class="visibility-form__role-checkbox-container"
|
class="visibility-form__role-list"
|
||||||
>
|
>
|
||||||
<template v-if="loadingRoles">
|
<template v-if="loadingRoles">
|
||||||
<div class="loading margin-bottom-1"></div>
|
<div class="loading margin-bottom-1"></div>
|
||||||
|
@ -40,7 +40,7 @@
|
||||||
<div
|
<div
|
||||||
v-for="roleName in allRoles"
|
v-for="roleName in allRoles"
|
||||||
:key="roleName"
|
:key="roleName"
|
||||||
class="visibility-form__role-checkbox-div"
|
class="visibility-form__role-checkbox"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:checked="isChecked(roleName)"
|
:checked="isChecked(roleName)"
|
||||||
|
@ -50,14 +50,11 @@
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="visibility-form__role-links">
|
<div class="visibility-form__actions">
|
||||||
<a @click.prevent="selectAllRoles">
|
<a @click.prevent="selectAllRoles">
|
||||||
{{ $t('visibilityForm.rolesSelectAll') }}
|
{{ $t('visibilityForm.rolesSelectAll') }}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a @click.prevent="deselectAllRoles">
|
||||||
class="visibility-form__role-links-deselect-all"
|
|
||||||
@click.prevent="deselectAllRoles"
|
|
||||||
>
|
|
||||||
{{ $t('visibilityForm.rolesDeselectAll') }}
|
{{ $t('visibilityForm.rolesDeselectAll') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -79,6 +76,7 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import visibilityForm from '@baserow/modules/builder/mixins/visibilityForm'
|
import visibilityForm from '@baserow/modules/builder/mixins/visibilityForm'
|
||||||
|
import elementForm from '@baserow/modules/builder/mixins/elementForm'
|
||||||
|
|
||||||
import {
|
import {
|
||||||
VISIBILITY_ALL,
|
VISIBILITY_ALL,
|
||||||
|
@ -87,7 +85,7 @@ import {
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'VisibilityForm',
|
name: 'VisibilityForm',
|
||||||
mixins: [visibilityForm],
|
mixins: [elementForm, visibilityForm],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
values: {
|
values: {
|
||||||
|
|
|
@ -228,7 +228,7 @@ export default {
|
||||||
CHOICE_OPTION_TYPES: () => CHOICE_OPTION_TYPES,
|
CHOICE_OPTION_TYPES: () => CHOICE_OPTION_TYPES,
|
||||||
element() {
|
element() {
|
||||||
return this.$store.getters['element/getElementById'](
|
return this.$store.getters['element/getElementById'](
|
||||||
this.page,
|
this.elementPage,
|
||||||
this.values.id
|
this.values.id
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -0,0 +1,131 @@
|
||||||
|
<template>
|
||||||
|
<form @submit.prevent @keydown.enter.prevent>
|
||||||
|
<FormGroup
|
||||||
|
small-label
|
||||||
|
:label="$t('multiPageContainerElementForm.display')"
|
||||||
|
class="margin-bottom-2"
|
||||||
|
required
|
||||||
|
>
|
||||||
|
<Dropdown v-model="computedPageShareType" :show-search="false" small>
|
||||||
|
<DropdownItem
|
||||||
|
v-for="item in pageShareTypes"
|
||||||
|
:key="item.value"
|
||||||
|
:name="item.label"
|
||||||
|
:value="item.value"
|
||||||
|
>
|
||||||
|
{{ item.label }}
|
||||||
|
</DropdownItem>
|
||||||
|
</Dropdown>
|
||||||
|
<template v-if="values.share_type !== 'all'">
|
||||||
|
<div class="multi-page-container-element-form__page-list">
|
||||||
|
<div
|
||||||
|
v-for="page in pages"
|
||||||
|
:key="page.id"
|
||||||
|
class="multi-page-container-element-form__page-checkbox"
|
||||||
|
>
|
||||||
|
<Checkbox
|
||||||
|
:checked="values.pages.includes(page.id)"
|
||||||
|
@input="togglePage(page)"
|
||||||
|
>
|
||||||
|
{{ page.name }}
|
||||||
|
</Checkbox>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="multi-page-container-element-form__actions">
|
||||||
|
<a @click.prevent="selectAll">
|
||||||
|
{{ $t('multiPageContainerElementForm.selectAll') }}
|
||||||
|
</a>
|
||||||
|
<a @click.prevent="deselectAll">
|
||||||
|
{{ $t('multiPageContainerElementForm.deselectAll') }}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</FormGroup>
|
||||||
|
</form>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
import elementForm from '@baserow/modules/builder/mixins/elementForm'
|
||||||
|
import { SHARE_TYPES } from '@baserow/modules/builder/enums'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
name: 'MultiPageContainerElementForm',
|
||||||
|
mixins: [elementForm],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
values: {
|
||||||
|
share_type: '',
|
||||||
|
pages: [],
|
||||||
|
styles: {},
|
||||||
|
},
|
||||||
|
allowedValues: ['share_type', 'pages', 'styles'],
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
computedPageShareType: {
|
||||||
|
get() {
|
||||||
|
return this.values.share_type
|
||||||
|
},
|
||||||
|
set(newValue) {
|
||||||
|
if (
|
||||||
|
[SHARE_TYPES.ONLY, SHARE_TYPES.EXCEPT].includes(newValue) &&
|
||||||
|
newValue !== this.values.share_type
|
||||||
|
) {
|
||||||
|
if (![SHARE_TYPES.ALL, undefined].includes(this.values.share_type)) {
|
||||||
|
// We want to invert the page selection if we change from except <-> only
|
||||||
|
this.values.pages = this.pageIds.filter(
|
||||||
|
(id) => !this.values.pages.includes(id)
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// Otherwise we want to select all or none.
|
||||||
|
this.values.pages =
|
||||||
|
newValue === SHARE_TYPES.ONLY ? [...this.pageIds] : []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.values.share_type = newValue
|
||||||
|
},
|
||||||
|
},
|
||||||
|
pageShareTypes() {
|
||||||
|
return [
|
||||||
|
{
|
||||||
|
label: this.$t('pageShareType.all'),
|
||||||
|
value: SHARE_TYPES.ALL,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('pageShareType.only'),
|
||||||
|
value: SHARE_TYPES.ONLY,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
label: this.$t('pageShareType.except'),
|
||||||
|
value: SHARE_TYPES.EXCEPT,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
pages() {
|
||||||
|
return this.$store.getters['page/getVisiblePages'](this.builder)
|
||||||
|
},
|
||||||
|
pageIds() {
|
||||||
|
return this.pages.map(({ id }) => id)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
togglePage(page) {
|
||||||
|
if (!this.values.pages.includes(page.id)) {
|
||||||
|
this.values.pages.push(page.id)
|
||||||
|
} else {
|
||||||
|
this.values.pages = this.values.pages.filter(
|
||||||
|
(pageId) => pageId !== page.id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
selectAll() {
|
||||||
|
this.values.pages = this.pageIds
|
||||||
|
},
|
||||||
|
deselectAll() {
|
||||||
|
this.values.pages = []
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
</script>
|
|
@ -15,8 +15,8 @@
|
||||||
<DataSourceDropdown
|
<DataSourceDropdown
|
||||||
v-model="values.data_source_id"
|
v-model="values.data_source_id"
|
||||||
small
|
small
|
||||||
:data-sources="listDataSources"
|
:shared-data-sources="listSharedDataSources"
|
||||||
:page="page"
|
:local-data-sources="listLocalDataSources"
|
||||||
>
|
>
|
||||||
<template #chooseValueState>
|
<template #chooseValueState>
|
||||||
{{ $t('recordSelectorElementForm.noDataSourceMessage') }}
|
{{ $t('recordSelectorElementForm.noDataSourceMessage') }}
|
||||||
|
@ -186,10 +186,18 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
// For now, RecordSelector only supports data sources that return arrays
|
// For now, RecordSelector only supports data sources that return arrays
|
||||||
listDataSources() {
|
listLocalDataSources() {
|
||||||
return this.dataSources.filter(
|
if (this.localDataSources === null) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return this.localDataSources.filter(
|
||||||
|
(dataSource) =>
|
||||||
|
this.$registry.get('service', dataSource.type).returnsList
|
||||||
|
)
|
||||||
|
},
|
||||||
|
listSharedDataSources() {
|
||||||
|
return this.sharedDataSources.filter(
|
||||||
(dataSource) =>
|
(dataSource) =>
|
||||||
dataSource.type &&
|
|
||||||
this.$registry.get('service', dataSource.type).returnsList
|
this.$registry.get('service', dataSource.type).returnsList
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -10,8 +10,8 @@
|
||||||
<DataSourceDropdown
|
<DataSourceDropdown
|
||||||
v-model="values.data_source_id"
|
v-model="values.data_source_id"
|
||||||
small
|
small
|
||||||
:data-sources="dataSources"
|
:shared-data-sources="sharedDataSources"
|
||||||
:page="page"
|
:local-data-sources="localDataSources"
|
||||||
>
|
>
|
||||||
<template #chooseValueState>
|
<template #chooseValueState>
|
||||||
{{ $t('collectionElementForm.noDataSourceMessage') }}
|
{{ $t('collectionElementForm.noDataSourceMessage') }}
|
||||||
|
@ -150,7 +150,6 @@
|
||||||
<script>
|
<script>
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
import { required, integer, minValue, maxValue } from 'vuelidate/lib/validators'
|
import { required, integer, minValue, maxValue } from 'vuelidate/lib/validators'
|
||||||
import elementForm from '@baserow/modules/builder/mixins/elementForm'
|
|
||||||
import collectionElementForm from '@baserow/modules/builder/mixins/collectionElementForm'
|
import collectionElementForm from '@baserow/modules/builder/mixins/collectionElementForm'
|
||||||
import DeviceSelector from '@baserow/modules/builder/components/page/header/DeviceSelector.vue'
|
import DeviceSelector from '@baserow/modules/builder/components/page/header/DeviceSelector.vue'
|
||||||
import { mapActions, mapGetters } from 'vuex'
|
import { mapActions, mapGetters } from 'vuex'
|
||||||
|
@ -170,7 +169,7 @@ export default {
|
||||||
InjectedFormulaInput,
|
InjectedFormulaInput,
|
||||||
ServiceSchemaPropertySelector,
|
ServiceSchemaPropertySelector,
|
||||||
},
|
},
|
||||||
mixins: [elementForm, collectionElementForm],
|
mixins: [collectionElementForm],
|
||||||
inject: ['applicationContext'],
|
inject: ['applicationContext'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -17,8 +17,8 @@
|
||||||
<DataSourceDropdown
|
<DataSourceDropdown
|
||||||
v-model="computedDataSourceId"
|
v-model="computedDataSourceId"
|
||||||
small
|
small
|
||||||
:data-sources="dataSources"
|
:shared-data-sources="sharedDataSources"
|
||||||
:page="page"
|
:local-data-sources="localDataSources"
|
||||||
>
|
>
|
||||||
<template #chooseValueState>
|
<template #chooseValueState>
|
||||||
{{ $t('collectionElementForm.noDataSourceMessage') }}
|
{{ $t('collectionElementForm.noDataSourceMessage') }}
|
||||||
|
@ -259,7 +259,6 @@ import {
|
||||||
minValue,
|
minValue,
|
||||||
maxValue,
|
maxValue,
|
||||||
} from 'vuelidate/lib/validators'
|
} from 'vuelidate/lib/validators'
|
||||||
import elementForm from '@baserow/modules/builder/mixins/elementForm'
|
|
||||||
import collectionElementForm from '@baserow/modules/builder/mixins/collectionElementForm'
|
import collectionElementForm from '@baserow/modules/builder/mixins/collectionElementForm'
|
||||||
import { TABLE_ORIENTATION } from '@baserow/modules/builder/enums'
|
import { TABLE_ORIENTATION } from '@baserow/modules/builder/enums'
|
||||||
import DeviceSelector from '@baserow/modules/builder/components/page/header/DeviceSelector.vue'
|
import DeviceSelector from '@baserow/modules/builder/components/page/header/DeviceSelector.vue'
|
||||||
|
@ -279,7 +278,7 @@ export default {
|
||||||
DeviceSelector,
|
DeviceSelector,
|
||||||
CustomStyle,
|
CustomStyle,
|
||||||
},
|
},
|
||||||
mixins: [elementForm, collectionElementForm],
|
mixins: [collectionElementForm],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
allowedValues: [
|
allowedValues: [
|
||||||
|
|
|
@ -60,7 +60,6 @@ export default {
|
||||||
name: 'PropertyOptionForm',
|
name: 'PropertyOptionForm',
|
||||||
components: { BaserowTable },
|
components: { BaserowTable },
|
||||||
mixins: [form],
|
mixins: [form],
|
||||||
inject: ['page'],
|
|
||||||
props: {
|
props: {
|
||||||
dataSource: {
|
dataSource: {
|
||||||
type: Object,
|
type: Object,
|
||||||
|
|
|
@ -78,7 +78,7 @@ export default {
|
||||||
name: 'Event',
|
name: 'Event',
|
||||||
components: { WorkflowAction },
|
components: { WorkflowAction },
|
||||||
mixins: [applicationContext],
|
mixins: [applicationContext],
|
||||||
inject: ['workspace', 'builder', 'page'],
|
inject: ['workspace', 'builder', 'elementPage'],
|
||||||
props: {
|
props: {
|
||||||
event: {
|
event: {
|
||||||
type: Event,
|
type: Event,
|
||||||
|
@ -128,7 +128,7 @@ export default {
|
||||||
this.addingAction = true
|
this.addingAction = true
|
||||||
try {
|
try {
|
||||||
await this.actionCreateWorkflowAction({
|
await this.actionCreateWorkflowAction({
|
||||||
page: this.page,
|
page: this.elementPage,
|
||||||
workflowActionType: DEFAULT_WORKFLOW_ACTION_TYPE,
|
workflowActionType: DEFAULT_WORKFLOW_ACTION_TYPE,
|
||||||
eventType: this.event.name,
|
eventType: this.event.name,
|
||||||
configuration: {
|
configuration: {
|
||||||
|
@ -143,7 +143,7 @@ export default {
|
||||||
async deleteWorkflowAction(workflowAction) {
|
async deleteWorkflowAction(workflowAction) {
|
||||||
try {
|
try {
|
||||||
await this.actionDeleteWorkflowAction({
|
await this.actionDeleteWorkflowAction({
|
||||||
page: this.page,
|
page: this.elementPage,
|
||||||
workflowAction,
|
workflowAction,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -153,7 +153,7 @@ export default {
|
||||||
async orderWorkflowActions(order) {
|
async orderWorkflowActions(order) {
|
||||||
try {
|
try {
|
||||||
await this.actionOrderWorkflowActions({
|
await this.actionOrderWorkflowActions({
|
||||||
page: this.page,
|
page: this.elementPage,
|
||||||
element: this.element,
|
element: this.element,
|
||||||
order,
|
order,
|
||||||
})
|
})
|
||||||
|
|
|
@ -29,7 +29,7 @@ export default {
|
||||||
mixins: [modal],
|
mixins: [modal],
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
page: null,
|
currentPage: null,
|
||||||
builder: this.builder,
|
builder: this.builder,
|
||||||
workspace: this.workspace,
|
workspace: this.workspace,
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,14 @@
|
||||||
<template>
|
<template>
|
||||||
<ThemeProvider class="page">
|
<ThemeProvider class="page">
|
||||||
|
<PageElement
|
||||||
|
v-for="element in headerElements"
|
||||||
|
:key="element.id"
|
||||||
|
:element="element"
|
||||||
|
:mode="mode"
|
||||||
|
:application-context-additions="{
|
||||||
|
recordIndexPath: [],
|
||||||
|
}"
|
||||||
|
/>
|
||||||
<PageElement
|
<PageElement
|
||||||
v-for="element in elements"
|
v-for="element in elements"
|
||||||
:key="element.id"
|
:key="element.id"
|
||||||
|
@ -9,6 +18,15 @@
|
||||||
recordIndexPath: [],
|
recordIndexPath: [],
|
||||||
}"
|
}"
|
||||||
/>
|
/>
|
||||||
|
<PageElement
|
||||||
|
v-for="element in footerElements"
|
||||||
|
:key="element.id"
|
||||||
|
:element="element"
|
||||||
|
:mode="mode"
|
||||||
|
:application-context-additions="{
|
||||||
|
recordIndexPath: [],
|
||||||
|
}"
|
||||||
|
/>
|
||||||
</ThemeProvider>
|
</ThemeProvider>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
@ -17,16 +35,13 @@ import PageElement from '@baserow/modules/builder/components/page/PageElement'
|
||||||
import ThemeProvider from '@baserow/modules/builder/components/theme/ThemeProvider'
|
import ThemeProvider from '@baserow/modules/builder/components/theme/ThemeProvider'
|
||||||
import { dimensionMixin } from '@baserow/modules/core/mixins/dimensions'
|
import { dimensionMixin } from '@baserow/modules/core/mixins/dimensions'
|
||||||
import _ from 'lodash'
|
import _ from 'lodash'
|
||||||
|
import { PAGE_PLACES } from '@baserow/modules/builder/enums'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
components: { ThemeProvider, PageElement },
|
components: { ThemeProvider, PageElement },
|
||||||
mixins: [dimensionMixin],
|
mixins: [dimensionMixin],
|
||||||
inject: ['builder', 'mode'],
|
inject: ['builder', 'mode'],
|
||||||
props: {
|
props: {
|
||||||
page: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
path: {
|
path: {
|
||||||
type: String,
|
type: String,
|
||||||
required: true,
|
required: true,
|
||||||
|
@ -39,6 +54,26 @@ export default {
|
||||||
type: Array,
|
type: Array,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
sharedElements: {
|
||||||
|
type: Array,
|
||||||
|
required: true,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
headerElements() {
|
||||||
|
return this.sharedElements.filter(
|
||||||
|
(element) =>
|
||||||
|
this.$registry.get('element', element.type).getPagePlace() ===
|
||||||
|
PAGE_PLACES.HEADER
|
||||||
|
)
|
||||||
|
},
|
||||||
|
footerElements() {
|
||||||
|
return this.sharedElements.filter(
|
||||||
|
(element) =>
|
||||||
|
this.$registry.get('element', element.type).getPagePlace() ===
|
||||||
|
PAGE_PLACES.FOOTER
|
||||||
|
)
|
||||||
|
},
|
||||||
},
|
},
|
||||||
watch: {
|
watch: {
|
||||||
'dimensions.width': {
|
'dimensions.width': {
|
||||||
|
|
|
@ -17,12 +17,14 @@
|
||||||
<div class="element__inner-wrapper">
|
<div class="element__inner-wrapper">
|
||||||
<component
|
<component
|
||||||
:is="component"
|
:is="component"
|
||||||
|
:key="element._.uid"
|
||||||
:element="element"
|
:element="element"
|
||||||
:children="children"
|
|
||||||
:application-context-additions="{
|
:application-context-additions="{
|
||||||
element,
|
element,
|
||||||
|
page: elementPage,
|
||||||
}"
|
}"
|
||||||
class="element"
|
class="element"
|
||||||
|
v-on="$listeners"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -49,9 +51,9 @@ import { mapGetters } from 'vuex'
|
||||||
export default {
|
export default {
|
||||||
name: 'PageElement',
|
name: 'PageElement',
|
||||||
mixins: [applicationContextMixin],
|
mixins: [applicationContextMixin],
|
||||||
inject: ['builder', 'page', 'mode'],
|
inject: ['builder', 'mode', 'currentPage'],
|
||||||
provide() {
|
provide() {
|
||||||
return { mode: this.elementMode }
|
return { mode: this.elementMode, elementPage: this.elementPage }
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
element: {
|
element: {
|
||||||
|
@ -79,16 +81,23 @@ export default {
|
||||||
this.elementMode === 'editing' ? 'editComponent' : 'component'
|
this.elementMode === 'editing' ? 'editComponent' : 'component'
|
||||||
return elementType[componentName]
|
return elementType[componentName]
|
||||||
},
|
},
|
||||||
children() {
|
|
||||||
return this.$store.getters['element/getChildren'](this.page, this.element)
|
|
||||||
},
|
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
loggedUser: 'userSourceUser/getUser',
|
loggedUser: 'userSourceUser/getUser',
|
||||||
}),
|
}),
|
||||||
|
elementPage() {
|
||||||
|
// We use the page from the element itself
|
||||||
|
return this.$store.getters['page/getById'](
|
||||||
|
this.builder,
|
||||||
|
this.element.page_id
|
||||||
|
)
|
||||||
|
},
|
||||||
|
elementType() {
|
||||||
|
return this.$registry.get('element', this.element.type)
|
||||||
|
},
|
||||||
isVisible() {
|
isVisible() {
|
||||||
const elementType = this.$registry.get('element', this.element.type)
|
const elementType = this.$registry.get('element', this.element.type)
|
||||||
const isInError = elementType.isInError({
|
const isInError = elementType.isInError({
|
||||||
page: this.page,
|
page: this.elementPage,
|
||||||
element: this.element,
|
element: this.element,
|
||||||
builder: this.builder,
|
builder: this.builder,
|
||||||
})
|
})
|
||||||
|
@ -97,6 +106,15 @@ export default {
|
||||||
return false
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
!this.elementType.isVisible({
|
||||||
|
element: this.element,
|
||||||
|
currentPage: this.currentPage,
|
||||||
|
})
|
||||||
|
) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
const isAuthenticated = this.$store.getters[
|
const isAuthenticated = this.$store.getters[
|
||||||
'userSourceUser/isAuthenticated'
|
'userSourceUser/isAuthenticated'
|
||||||
](this.builder)
|
](this.builder)
|
||||||
|
|
|
@ -1,10 +1,10 @@
|
||||||
<template>
|
<template>
|
||||||
<ThemeProvider
|
<div
|
||||||
class="page-preview__wrapper"
|
class="page-preview__wrapper"
|
||||||
:class="`page-preview__wrapper--${deviceType.type}`"
|
:class="`page-preview__wrapper--${deviceType.type}`"
|
||||||
@click.self="actionSelectElement({ element: null })"
|
@click.self="actionSelectElement({ element: null })"
|
||||||
>
|
>
|
||||||
<PreviewNavigationBar :page="page" :style="{ maxWidth }" />
|
<PreviewNavigationBar :page="currentPage" :style="{ maxWidth }" />
|
||||||
<div ref="preview" class="page-preview" :style="{ 'max-width': maxWidth }">
|
<div ref="preview" class="page-preview" :style="{ 'max-width': maxWidth }">
|
||||||
<div
|
<div
|
||||||
ref="previewScaled"
|
ref="previewScaled"
|
||||||
|
@ -12,36 +12,101 @@
|
||||||
tabindex="0"
|
tabindex="0"
|
||||||
@keydown="handleKeyDown"
|
@keydown="handleKeyDown"
|
||||||
>
|
>
|
||||||
<CallToAction
|
<ThemeProvider class="page">
|
||||||
v-if="!elements.length"
|
<template v-if="headerElements.length !== 0">
|
||||||
class="page-preview__empty"
|
<header
|
||||||
icon="baserow-icon-plus"
|
class="page__header"
|
||||||
icon-color="neutral"
|
:class="{
|
||||||
icon-size="large"
|
'page__header--element-selected':
|
||||||
icon-rounded
|
pageSectionWithSelectedElement === 'header',
|
||||||
@click="$refs.addElementModal.show()"
|
}"
|
||||||
>
|
>
|
||||||
{{ $t('pagePreview.emptyMessage') }}
|
<ElementPreview
|
||||||
</CallToAction>
|
v-for="(element, index) in headerElements"
|
||||||
<div v-else class="page">
|
:key="element.id"
|
||||||
<ElementPreview
|
:element="element"
|
||||||
v-for="(element, index) in elements"
|
:is-first-element="index === 0"
|
||||||
:key="element.id"
|
:is-copying="copyingElementIndex === index"
|
||||||
is-root-element
|
:application-context-additions="{
|
||||||
:element="element"
|
recordIndexPath: [],
|
||||||
:is-first-element="index === 0"
|
}"
|
||||||
:is-last-element="index === elements.length - 1"
|
@move="moveElement($event)"
|
||||||
:is-copying="copyingElementIndex === index"
|
/>
|
||||||
:application-context-additions="{
|
</header>
|
||||||
recordIndexPath: [],
|
<div class="page-preview__separator">
|
||||||
}"
|
<span class="page-preview__separator-label">
|
||||||
@move="moveElement($event)"
|
{{ $t('pagePreview.header') }}
|
||||||
/>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="elements.length === 0">
|
||||||
|
<CallToAction
|
||||||
|
class="page-preview__empty"
|
||||||
|
icon="baserow-icon-plus"
|
||||||
|
icon-color="neutral"
|
||||||
|
icon-size="large"
|
||||||
|
icon-rounded
|
||||||
|
@click="$refs.addElementModal.show()"
|
||||||
|
>
|
||||||
|
{{ $t('pagePreview.emptyMessage') }}
|
||||||
|
</CallToAction>
|
||||||
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<div
|
||||||
|
class="page__content"
|
||||||
|
:class="{
|
||||||
|
'page__content--element-selected':
|
||||||
|
pageSectionWithSelectedElement === 'content',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<ElementPreview
|
||||||
|
v-for="(element, index) in elements"
|
||||||
|
:key="element.id"
|
||||||
|
:element="element"
|
||||||
|
:is-first-element="index === 0 && headerElements.length === 0"
|
||||||
|
:is-copying="copyingElementIndex === index"
|
||||||
|
:application-context-additions="{
|
||||||
|
recordIndexPath: [],
|
||||||
|
}"
|
||||||
|
@move="moveElement($event)"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
<template v-if="footerElements.length !== 0">
|
||||||
|
<div class="page-preview__separator">
|
||||||
|
<span class="page-preview__separator-label">
|
||||||
|
{{ $t('pagePreview.footer') }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<footer
|
||||||
|
class="page__footer"
|
||||||
|
:class="{
|
||||||
|
'page__footer--element-selected':
|
||||||
|
pageSectionWithSelectedElement === 'footer',
|
||||||
|
}"
|
||||||
|
>
|
||||||
|
<ElementPreview
|
||||||
|
v-for="(element, index) in footerElements"
|
||||||
|
:key="element.id"
|
||||||
|
:element="element"
|
||||||
|
:is-first-element="
|
||||||
|
index === 0 &&
|
||||||
|
headerElements.length === 0 &&
|
||||||
|
elements.length === 0
|
||||||
|
"
|
||||||
|
:is-copying="copyingElementIndex === index"
|
||||||
|
:application-context-additions="{
|
||||||
|
recordIndexPath: [],
|
||||||
|
}"
|
||||||
|
@move="moveElement($event)"
|
||||||
|
/>
|
||||||
|
</footer>
|
||||||
|
</template>
|
||||||
|
</ThemeProvider>
|
||||||
</div>
|
</div>
|
||||||
<AddElementModal ref="addElementModal" :page="page" />
|
<AddElementModal ref="addElementModal" :page="currentPage" />
|
||||||
</div>
|
</div>
|
||||||
</ThemeProvider>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
|
@ -50,7 +115,7 @@ import { mapActions, mapGetters } from 'vuex'
|
||||||
import ElementPreview from '@baserow/modules/builder/components/elements/ElementPreview'
|
import ElementPreview from '@baserow/modules/builder/components/elements/ElementPreview'
|
||||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||||
import PreviewNavigationBar from '@baserow/modules/builder/components/page/PreviewNavigationBar'
|
import PreviewNavigationBar from '@baserow/modules/builder/components/page/PreviewNavigationBar'
|
||||||
import { PLACEMENTS } from '@baserow/modules/builder/enums'
|
import { DIRECTIONS, PAGE_PLACES } from '@baserow/modules/builder/enums'
|
||||||
import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal.vue'
|
import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal.vue'
|
||||||
import ThemeProvider from '@baserow/modules/builder/components/theme/ThemeProvider.vue'
|
import ThemeProvider from '@baserow/modules/builder/components/theme/ThemeProvider.vue'
|
||||||
|
|
||||||
|
@ -62,7 +127,7 @@ export default {
|
||||||
ElementPreview,
|
ElementPreview,
|
||||||
PreviewNavigationBar,
|
PreviewNavigationBar,
|
||||||
},
|
},
|
||||||
inject: ['page', 'workspace'],
|
inject: ['builder', 'currentPage', 'workspace'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
// The element that is currently being copied
|
// The element that is currently being copied
|
||||||
|
@ -73,7 +138,7 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
PLACEMENTS: () => PLACEMENTS,
|
DIRECTIONS: () => DIRECTIONS,
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
deviceTypeSelected: 'page/getDeviceTypeSelected',
|
deviceTypeSelected: 'page/getDeviceTypeSelected',
|
||||||
elementSelected: 'element/getSelected',
|
elementSelected: 'element/getSelected',
|
||||||
|
@ -81,11 +146,84 @@ export default {
|
||||||
getClosestSiblingElement: 'element/getClosestSiblingElement',
|
getClosestSiblingElement: 'element/getClosestSiblingElement',
|
||||||
}),
|
}),
|
||||||
elements() {
|
elements() {
|
||||||
return this.$store.getters['element/getRootElements'](this.page)
|
return this.$store.getters['element/getRootElements'](this.currentPage)
|
||||||
|
},
|
||||||
|
sharedPage() {
|
||||||
|
return this.$store.getters['page/getSharedPage'](this.builder)
|
||||||
|
},
|
||||||
|
sharedElements() {
|
||||||
|
return this.$store.getters['element/getRootElements'](this.sharedPage)
|
||||||
|
},
|
||||||
|
headerElements() {
|
||||||
|
return this.sharedElements.filter(
|
||||||
|
(element) =>
|
||||||
|
this.$registry.get('element', element.type).getPagePlace() ===
|
||||||
|
PAGE_PLACES.HEADER
|
||||||
|
)
|
||||||
|
},
|
||||||
|
footerElements() {
|
||||||
|
return this.sharedElements.filter(
|
||||||
|
(element) =>
|
||||||
|
this.$registry.get('element', element.type).getPagePlace() ===
|
||||||
|
PAGE_PLACES.FOOTER
|
||||||
|
)
|
||||||
},
|
},
|
||||||
elementSelectedId() {
|
elementSelectedId() {
|
||||||
return this.elementSelected?.id
|
return this.elementSelected?.id
|
||||||
},
|
},
|
||||||
|
elementSelectedType() {
|
||||||
|
if (!this.elementSelected) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return this.$registry.get('element', this.elementSelected.type)
|
||||||
|
},
|
||||||
|
pageSectionWithSelectedElement() {
|
||||||
|
if (!this.elementSelected) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
if (this.elementSelected.page_id === this.currentPage.id) {
|
||||||
|
return PAGE_PLACES.CONTENT
|
||||||
|
}
|
||||||
|
const ancestorWithPagePlace = this.$store.getters['element/getAncestors'](
|
||||||
|
this.elementSelectedPage,
|
||||||
|
this.elementSelected,
|
||||||
|
{
|
||||||
|
includeSelf: true,
|
||||||
|
predicate: (parentElement) => {
|
||||||
|
return (
|
||||||
|
this.$registry
|
||||||
|
.get('element', parentElement.type)
|
||||||
|
.getPagePlace() !== PAGE_PLACES.CONTENT
|
||||||
|
)
|
||||||
|
},
|
||||||
|
}
|
||||||
|
)[0]
|
||||||
|
|
||||||
|
return this.$registry
|
||||||
|
.get('element', ancestorWithPagePlace.type)
|
||||||
|
.getPagePlace()
|
||||||
|
},
|
||||||
|
elementsAround() {
|
||||||
|
if (!this.elementSelected) {
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
return this.elementSelectedType.getElementsAround({
|
||||||
|
builder: this.builder,
|
||||||
|
page: this.currentPage,
|
||||||
|
element: this.elementSelected,
|
||||||
|
withSharedPage: true,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
elementSelectedPage() {
|
||||||
|
if (this.elementSelected) {
|
||||||
|
// We use the page from the element itself
|
||||||
|
return this.$store.getters['page/getById'](
|
||||||
|
this.builder,
|
||||||
|
this.elementSelected.page_id
|
||||||
|
)
|
||||||
|
}
|
||||||
|
return null
|
||||||
|
},
|
||||||
deviceType() {
|
deviceType() {
|
||||||
return this.deviceTypeSelected
|
return this.deviceTypeSelected
|
||||||
? this.$registry.get('device', this.deviceTypeSelected)
|
? this.$registry.get('device', this.deviceTypeSelected)
|
||||||
|
@ -101,14 +239,14 @@ export default {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
return this.$store.getters['element/getElementById'](
|
return this.$store.getters['element/getElementById'](
|
||||||
this.page,
|
this.elementSelectedPage,
|
||||||
this.elementSelected.parent_element_id
|
this.elementSelected.parent_element_id
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
canCreateElement() {
|
canCreateElement() {
|
||||||
return this.$hasPermission(
|
return this.$hasPermission(
|
||||||
'builder.page.create_element',
|
'builder.page.create_element',
|
||||||
this.page,
|
this.currentPage,
|
||||||
this.workspace.id
|
this.workspace.id
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -156,9 +294,8 @@ export default {
|
||||||
...mapActions({
|
...mapActions({
|
||||||
actionDuplicateElement: 'element/duplicate',
|
actionDuplicateElement: 'element/duplicate',
|
||||||
actionDeleteElement: 'element/delete',
|
actionDeleteElement: 'element/delete',
|
||||||
actionMoveElement: 'element/moveElement',
|
|
||||||
actionSelectElement: 'element/select',
|
actionSelectElement: 'element/select',
|
||||||
actionSelectNextElement: 'element/selectNextElement',
|
actionMoveElement: 'element/move',
|
||||||
}),
|
}),
|
||||||
preventScrollIfFocused(e) {
|
preventScrollIfFocused(e) {
|
||||||
if (this.$refs.previewScaled === document.activeElement) {
|
if (this.$refs.previewScaled === document.activeElement) {
|
||||||
|
@ -199,62 +336,57 @@ export default {
|
||||||
previewScaled.style.width = `${currentWidth / scale}px`
|
previewScaled.style.width = `${currentWidth / scale}px`
|
||||||
previewScaled.style.height = `${currentHeight / scale}px`
|
previewScaled.style.height = `${currentHeight / scale}px`
|
||||||
},
|
},
|
||||||
|
async moveElement({ element, direction }) {
|
||||||
|
if (
|
||||||
|
!this.$hasPermission(
|
||||||
|
'builder.page.element.update',
|
||||||
|
element,
|
||||||
|
this.workspace.id
|
||||||
|
)
|
||||||
|
) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
async moveElement(placement) {
|
const elementPage = this.$store.getters['page/getById'](
|
||||||
|
this.builder,
|
||||||
|
element.page_id
|
||||||
|
)
|
||||||
|
|
||||||
|
const elementType = this.$registry.get('element', element.type)
|
||||||
|
|
||||||
|
const nextPlaces = elementType.getNextPlaces({
|
||||||
|
builder: this.builder,
|
||||||
|
page: this.currentPage,
|
||||||
|
element,
|
||||||
|
})
|
||||||
|
if (nextPlaces[direction]) {
|
||||||
|
try {
|
||||||
|
await this.actionMoveElement({
|
||||||
|
page: elementPage,
|
||||||
|
elementId: element.id,
|
||||||
|
...nextPlaces[direction],
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
notifyIf(error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
async moveSelectedElement(direction) {
|
||||||
if (!this.elementSelected?.id || !this.canUpdateSelectedElement) {
|
if (!this.elementSelected?.id || !this.canUpdateSelectedElement) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
await this.moveElement({
|
||||||
const elementType = this.$registry.get(
|
element: this.elementSelected,
|
||||||
'element',
|
direction,
|
||||||
this.elementSelected.type
|
})
|
||||||
)
|
|
||||||
const placementsDisabled = elementType.getPlacementsDisabled(
|
|
||||||
this.page,
|
|
||||||
this.elementSelected
|
|
||||||
)
|
|
||||||
|
|
||||||
if (placementsDisabled.includes(placement)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.actionMoveElement({
|
|
||||||
page: this.page,
|
|
||||||
element: this.elementSelected,
|
|
||||||
placement,
|
|
||||||
})
|
|
||||||
await this.actionSelectElement({ element: this.elementSelected })
|
|
||||||
} catch (error) {
|
|
||||||
notifyIf(error)
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
async selectNextElement(placement) {
|
async moveSelection(direction) {
|
||||||
if (!this.elementSelected?.id) {
|
if (!this.elementSelected?.id) {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
const nextElement = this.elementsAround[direction]
|
||||||
const elementType = this.$registry.get(
|
if (nextElement) {
|
||||||
'element',
|
await this.actionSelectElement({ element: nextElement })
|
||||||
this.elementSelected.type
|
|
||||||
)
|
|
||||||
const placementsDisabled = elementType.getPlacementsDisabled(
|
|
||||||
this.page,
|
|
||||||
this.elementSelected
|
|
||||||
)
|
|
||||||
|
|
||||||
if (placementsDisabled.includes(placement)) {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
|
||||||
await this.actionSelectNextElement({
|
|
||||||
page: this.page,
|
|
||||||
element: this.elementSelected,
|
|
||||||
placement,
|
|
||||||
})
|
|
||||||
} catch (error) {
|
|
||||||
notifyIf(error)
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
async duplicateElement() {
|
async duplicateElement() {
|
||||||
|
@ -265,7 +397,7 @@ export default {
|
||||||
this.isDuplicating = true
|
this.isDuplicating = true
|
||||||
try {
|
try {
|
||||||
await this.actionDuplicateElement({
|
await this.actionDuplicateElement({
|
||||||
page: this.page,
|
page: this.elementSelectedPage,
|
||||||
elementId: this.elementSelected.id,
|
elementId: this.elementSelected.id,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
@ -278,12 +410,15 @@ export default {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
const siblingElementToSelect = this.getClosestSiblingElement(
|
const siblingElementToSelect =
|
||||||
this.page,
|
this.elementsAround[DIRECTIONS.AFTER] ||
|
||||||
this.elementSelected
|
this.elementsAround[DIRECTIONS.BEFORE] ||
|
||||||
)
|
this.elementsAround[DIRECTIONS.LEFT] ||
|
||||||
|
this.elementsAround[DIRECTIONS.RIGHT] ||
|
||||||
|
this.parentOfElementSelected
|
||||||
|
|
||||||
await this.actionDeleteElement({
|
await this.actionDeleteElement({
|
||||||
page: this.page,
|
page: this.elementSelectedPage,
|
||||||
elementId: this.elementSelected.id,
|
elementId: this.elementSelected.id,
|
||||||
})
|
})
|
||||||
if (siblingElementToSelect?.id) {
|
if (siblingElementToSelect?.id) {
|
||||||
|
@ -299,7 +434,10 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
selectChildElement() {
|
selectChildElement() {
|
||||||
const children = this.getChildren(this.page, this.elementSelected)
|
const children = this.getChildren(
|
||||||
|
this.elementSelectedPage,
|
||||||
|
this.elementSelected
|
||||||
|
)
|
||||||
if (children.length) {
|
if (children.length) {
|
||||||
this.actionSelectElement({ element: children[0] })
|
this.actionSelectElement({ element: children[0] })
|
||||||
}
|
}
|
||||||
|
@ -310,30 +448,30 @@ export default {
|
||||||
switch (e.key) {
|
switch (e.key) {
|
||||||
case 'ArrowUp':
|
case 'ArrowUp':
|
||||||
if (alternateAction) {
|
if (alternateAction) {
|
||||||
this.moveElement(PLACEMENTS.BEFORE)
|
this.moveSelectedElement(DIRECTIONS.BEFORE)
|
||||||
} else {
|
} else {
|
||||||
this.selectNextElement(PLACEMENTS.BEFORE)
|
this.moveSelection(DIRECTIONS.BEFORE)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'ArrowDown':
|
case 'ArrowDown':
|
||||||
if (alternateAction) {
|
if (alternateAction) {
|
||||||
this.moveElement(PLACEMENTS.AFTER)
|
this.moveSelectedElement(DIRECTIONS.AFTER)
|
||||||
} else {
|
} else {
|
||||||
this.selectNextElement(PLACEMENTS.AFTER)
|
this.moveSelection(DIRECTIONS.AFTER)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'ArrowLeft':
|
case 'ArrowLeft':
|
||||||
if (alternateAction) {
|
if (alternateAction) {
|
||||||
this.moveElement(PLACEMENTS.LEFT)
|
this.moveSelectedElement(DIRECTIONS.LEFT)
|
||||||
} else {
|
} else {
|
||||||
this.selectNextElement(PLACEMENTS.LEFT)
|
this.moveSelection(DIRECTIONS.LEFT)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'ArrowRight':
|
case 'ArrowRight':
|
||||||
if (alternateAction) {
|
if (alternateAction) {
|
||||||
this.moveElement(PLACEMENTS.RIGHT)
|
this.moveSelectedElement(DIRECTIONS.RIGHT)
|
||||||
} else {
|
} else {
|
||||||
this.selectNextElement(PLACEMENTS.RIGHT)
|
this.moveSelection(DIRECTIONS.RIGHT)
|
||||||
}
|
}
|
||||||
break
|
break
|
||||||
case 'Backspace':
|
case 'Backspace':
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
<template>
|
<template>
|
||||||
<PageTemplateContent
|
<PageTemplateContent
|
||||||
v-if="!loading && workspace && page && builder"
|
v-if="!loading && workspace && currentPage && builder"
|
||||||
:workspace="workspace"
|
:workspace="workspace"
|
||||||
:builder="builder"
|
:builder="builder"
|
||||||
:page="page"
|
:current-page="currentPage"
|
||||||
:mode="mode"
|
:mode="mode"
|
||||||
/>
|
/>
|
||||||
<PageSkeleton v-else />
|
<PageSkeleton v-else />
|
||||||
|
@ -32,7 +32,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
workspace: null,
|
workspace: null,
|
||||||
builder: null,
|
builder: null,
|
||||||
page: null,
|
currentPage: null,
|
||||||
mode,
|
mode,
|
||||||
loading: true,
|
loading: true,
|
||||||
}
|
}
|
||||||
|
@ -104,7 +104,7 @@ export default {
|
||||||
)
|
)
|
||||||
|
|
||||||
this.builder = builder
|
this.builder = builder
|
||||||
this.page = page
|
this.currentPage = page
|
||||||
this.workspace = builder.workspace
|
this.workspace = builder.workspace
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// In case of a network error we want to fail hard.
|
// In case of a network error we want to fail hard.
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div v-if="page" :key="page.id" class="page-template">
|
<div v-if="currentPage" :key="currentPage.id" class="page-template">
|
||||||
<PageHeader :page="page" />
|
<PageHeader />
|
||||||
<div class="layout__col-2-2 page-editor__content">
|
<div class="layout__col-2-2 page-editor__content">
|
||||||
<div :style="{ width: `calc(100% - ${panelWidth}px)` }">
|
<div :style="{ width: `calc(100% - ${panelWidth}px)` }">
|
||||||
<PagePreview />
|
<PagePreview />
|
||||||
|
@ -32,10 +32,13 @@ export default {
|
||||||
return {
|
return {
|
||||||
workspace: this.workspace,
|
workspace: this.workspace,
|
||||||
builder: this.builder,
|
builder: this.builder,
|
||||||
page: this.page,
|
currentPage: this.currentPage,
|
||||||
mode,
|
mode,
|
||||||
formulaComponent: ApplicationBuilderFormulaInput,
|
formulaComponent: ApplicationBuilderFormulaInput,
|
||||||
applicationContext: { builder: this.builder, page: this.page, mode },
|
applicationContext: {
|
||||||
|
builder: this.builder,
|
||||||
|
mode,
|
||||||
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
props: {
|
props: {
|
||||||
|
@ -47,7 +50,7 @@ export default {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
page: {
|
currentPage: {
|
||||||
type: Object,
|
type: Object,
|
||||||
required: true,
|
required: true,
|
||||||
},
|
},
|
||||||
|
@ -63,7 +66,7 @@ export default {
|
||||||
applicationContext() {
|
applicationContext() {
|
||||||
return {
|
return {
|
||||||
builder: this.builder,
|
builder: this.builder,
|
||||||
page: this.page,
|
page: this.currentPage,
|
||||||
mode,
|
mode,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -76,7 +79,9 @@ export default {
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
dataSources() {
|
dataSources() {
|
||||||
return this.$store.getters['dataSource/getPageDataSources'](this.page)
|
return this.$store.getters['dataSource/getPageDataSources'](
|
||||||
|
this.currentPage
|
||||||
|
)
|
||||||
},
|
},
|
||||||
dispatchContext() {
|
dispatchContext() {
|
||||||
return DataProviderType.getAllDataSourceDispatchContext(
|
return DataProviderType.getAllDataSourceDispatchContext(
|
||||||
|
@ -96,7 +101,7 @@ export default {
|
||||||
this.$store.dispatch(
|
this.$store.dispatch(
|
||||||
'dataSourceContent/debouncedFetchPageDataSourceContent',
|
'dataSourceContent/debouncedFetchPageDataSourceContent',
|
||||||
{
|
{
|
||||||
page: this.page,
|
page: this.currentPage,
|
||||||
data: newDispatchContext,
|
data: newDispatchContext,
|
||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
}
|
}
|
||||||
|
|
|
@ -79,7 +79,7 @@ import { DEFAULT_USER_ROLE_PREFIX } from '@baserow/modules/builder/constants'
|
||||||
export default {
|
export default {
|
||||||
name: 'UserSourceUsersContext',
|
name: 'UserSourceUsersContext',
|
||||||
mixins: [context],
|
mixins: [context],
|
||||||
inject: ['page', 'builder'],
|
inject: ['builder'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
state: null,
|
state: null,
|
||||||
|
|
|
@ -10,24 +10,49 @@
|
||||||
ref="search"
|
ref="search"
|
||||||
v-model="search"
|
v-model="search"
|
||||||
type="text"
|
type="text"
|
||||||
class="elements-list__search-input"
|
class="elements-context__search-input"
|
||||||
:placeholder="$t('elementsContext.searchPlaceholder')"
|
:placeholder="$t('elementsContext.searchPlaceholder')"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<ElementsList
|
<div class="elements-context__elements">
|
||||||
v-if="elementsVisible"
|
<ElementsList
|
||||||
:elements="rootElements"
|
v-if="headerElementsVisible"
|
||||||
:filtered-search-elements="filteredSearchElements"
|
:elements="headerElements"
|
||||||
@select="selectElement($event)"
|
:filtered-search-elements="filteredHeaderElements"
|
||||||
/>
|
@select="selectElement($event)"
|
||||||
<div v-else class="context__description">
|
/>
|
||||||
{{ $t('elementsContext.noElements') }}
|
<ElementsList
|
||||||
|
v-if="contentElementsVisible"
|
||||||
|
:elements="rootElements"
|
||||||
|
:filtered-search-elements="filteredContentElements"
|
||||||
|
@select="selectElement($event)"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="!contentElementsVisible && !isSearching"
|
||||||
|
class="elements-list elements-list--empty"
|
||||||
|
>
|
||||||
|
{{ $t('elementsContext.noPageElements') }}
|
||||||
|
</div>
|
||||||
|
<ElementsList
|
||||||
|
v-if="footerElementsVisible"
|
||||||
|
:elements="footerElements"
|
||||||
|
:filtered-search-elements="filteredFooterElements"
|
||||||
|
@select="selectElement($event)"
|
||||||
|
/>
|
||||||
|
<div
|
||||||
|
v-if="!elementsVisible && isSearching"
|
||||||
|
class="elements-list elements-list--empty"
|
||||||
|
>
|
||||||
|
{{ $t('elementsContext.noElements') }}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
v-if="$hasPermission('builder.page.create_element', page, workspace.id)"
|
v-if="
|
||||||
class="elements-list__footer"
|
$hasPermission('builder.page.create_element', currentPage, workspace.id)
|
||||||
|
"
|
||||||
|
class="elements-context__footer"
|
||||||
>
|
>
|
||||||
<div class="elements-list__footer-create">
|
<div class="elements-context__footer-create">
|
||||||
<AddElementButton
|
<AddElementButton
|
||||||
:class="{
|
:class="{
|
||||||
'margin-top-1': !elementsVisible,
|
'margin-top-1': !elementsVisible,
|
||||||
|
@ -37,9 +62,11 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<AddElementModal
|
<AddElementModal
|
||||||
v-if="$hasPermission('builder.page.create_element', page, workspace.id)"
|
v-if="
|
||||||
|
$hasPermission('builder.page.create_element', currentPage, workspace.id)
|
||||||
|
"
|
||||||
ref="addElementModal"
|
ref="addElementModal"
|
||||||
:page="page"
|
:page="currentPage"
|
||||||
@element-added="onElementAdded"
|
@element-added="onElementAdded"
|
||||||
/>
|
/>
|
||||||
</Context>
|
</Context>
|
||||||
|
@ -52,27 +79,110 @@ import AddElementButton from '@baserow/modules/builder/components/elements/AddEl
|
||||||
import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal'
|
import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal'
|
||||||
import { mapActions } from 'vuex'
|
import { mapActions } from 'vuex'
|
||||||
import { isSubstringOfStrings } from '@baserow/modules/core/utils/string'
|
import { isSubstringOfStrings } from '@baserow/modules/core/utils/string'
|
||||||
|
import { PAGE_PLACES } from '@baserow/modules/builder/enums'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'ElementsContext',
|
name: 'ElementsContext',
|
||||||
components: { AddElementModal, AddElementButton, ElementsList },
|
components: { AddElementModal, AddElementButton, ElementsList },
|
||||||
mixins: [context],
|
mixins: [context],
|
||||||
inject: ['workspace', 'page', 'builder', 'mode'],
|
inject: ['workspace', 'currentPage', 'builder', 'mode'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
search: null,
|
search: null,
|
||||||
addingElementType: null,
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
|
isSearching() {
|
||||||
|
return Boolean(this.search)
|
||||||
|
},
|
||||||
elementsVisible() {
|
elementsVisible() {
|
||||||
return (
|
return (
|
||||||
(this.search && this.filteredSearchElements.length) ||
|
(this.search &&
|
||||||
|
(this.filteredContentElements.length ||
|
||||||
|
this.filteredHeaderElements.length ||
|
||||||
|
this.filteredFooterElements.length)) ||
|
||||||
|
(!this.search &&
|
||||||
|
(this.rootElements.length ||
|
||||||
|
this.headerElements.length ||
|
||||||
|
this.footerElements.length))
|
||||||
|
)
|
||||||
|
},
|
||||||
|
contentElementsVisible() {
|
||||||
|
return (
|
||||||
|
(this.search && this.filteredContentElements.length) ||
|
||||||
(!this.search && this.rootElements.length)
|
(!this.search && this.rootElements.length)
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
headerElementsVisible() {
|
||||||
|
return (
|
||||||
|
(this.search && this.filteredHeaderElements.length) ||
|
||||||
|
(!this.search && this.headerElements.length)
|
||||||
|
)
|
||||||
|
},
|
||||||
|
footerElementsVisible() {
|
||||||
|
return (
|
||||||
|
(this.search && this.filteredFooterElements.length) ||
|
||||||
|
(!this.search && this.footerElements.length)
|
||||||
|
)
|
||||||
|
},
|
||||||
rootElements() {
|
rootElements() {
|
||||||
return this.$store.getters['element/getRootElements'](this.page)
|
return this.$store.getters['element/getRootElements'](this.currentPage)
|
||||||
|
},
|
||||||
|
sharedPage() {
|
||||||
|
return this.$store.getters['page/getSharedPage'](this.builder)
|
||||||
|
},
|
||||||
|
sharedElements() {
|
||||||
|
return this.$store.getters['element/getRootElements'](this.sharedPage)
|
||||||
|
},
|
||||||
|
headerElements() {
|
||||||
|
return this.sharedElements.filter(
|
||||||
|
(element) =>
|
||||||
|
this.$registry.get('element', element.type).getPagePlace() ===
|
||||||
|
PAGE_PLACES.HEADER
|
||||||
|
)
|
||||||
|
},
|
||||||
|
footerElements() {
|
||||||
|
return this.sharedElements.filter(
|
||||||
|
(element) =>
|
||||||
|
this.$registry.get('element', element.type).getPagePlace() ===
|
||||||
|
PAGE_PLACES.FOOTER
|
||||||
|
)
|
||||||
|
},
|
||||||
|
filteredContentElements() {
|
||||||
|
return this.filterElements(this.rootElements, this.currentPage)
|
||||||
|
},
|
||||||
|
filteredHeaderElements() {
|
||||||
|
return this.filterElements(this.headerElements, this.sharedPage)
|
||||||
|
},
|
||||||
|
filteredFooterElements() {
|
||||||
|
return this.filterElements(this.footerElements, this.sharedPage)
|
||||||
|
},
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
...mapActions({
|
||||||
|
actionSelectElement: 'element/select',
|
||||||
|
}),
|
||||||
|
/*
|
||||||
|
* Given an element, this method will return the corpus we want to
|
||||||
|
* search against when a user enters a search query. Should we want
|
||||||
|
* to search both the 'display name' and the element type name, this
|
||||||
|
* method can be easily adapted to combine the two and return it.
|
||||||
|
*/
|
||||||
|
getElementCorpus(element, page) {
|
||||||
|
const elementType = this.$registry.get('element', element.type)
|
||||||
|
return elementType.getDisplayName(element, {
|
||||||
|
builder: this.builder,
|
||||||
|
page,
|
||||||
|
mode: this.mode,
|
||||||
|
element,
|
||||||
|
})
|
||||||
|
},
|
||||||
|
onElementAdded() {
|
||||||
|
this.hide()
|
||||||
|
},
|
||||||
|
selectElement(element) {
|
||||||
|
this.actionSelectElement({ element })
|
||||||
|
this.hide()
|
||||||
},
|
},
|
||||||
/*
|
/*
|
||||||
* When a user searches for elements in the list, this computed method
|
* When a user searches for elements in the list, this computed method
|
||||||
|
@ -102,34 +212,30 @@ export default {
|
||||||
* - Repeat
|
* - Repeat
|
||||||
* - Image
|
* - Image
|
||||||
*/
|
*/
|
||||||
filteredSearchElements() {
|
filterElements(elements, page) {
|
||||||
let filteredToElementIds = []
|
let filteredToElementIds = []
|
||||||
if (
|
if (!this.search) {
|
||||||
this.search === '' ||
|
|
||||||
this.search === null ||
|
|
||||||
this.search === undefined
|
|
||||||
) {
|
|
||||||
// If there's no search query, then there are no
|
// If there's no search query, then there are no
|
||||||
// elements to narrow the results down to.
|
// elements to narrow the results down to.
|
||||||
return filteredToElementIds
|
return filteredToElementIds
|
||||||
}
|
}
|
||||||
|
|
||||||
// Iterate over all the root-level elements.
|
// Iterate over all the root-level elements.
|
||||||
this.rootElements.forEach((rootElement) => {
|
elements.forEach((element) => {
|
||||||
// Find this element's descendants and loop over them.
|
// Find this element's descendants and loop over them.
|
||||||
const descendants = this.$store.getters['element/getDescendants'](
|
const descendants = this.$store.getters['element/getDescendants'](
|
||||||
this.page,
|
page,
|
||||||
rootElement
|
element
|
||||||
)
|
)
|
||||||
descendants.forEach((descendant) => {
|
descendants.forEach((descendant) => {
|
||||||
// Build this descendant's corpus (for now, display name only)
|
// Build this descendant's corpus (for now, display name only)
|
||||||
// and check if it matches the search query.
|
// and check if it matches the search query.
|
||||||
const descendantCorpus = this.getElementCorpus(descendant)
|
const descendantCorpus = this.getElementCorpus(descendant, page)
|
||||||
if (isSubstringOfStrings([descendantCorpus], this.search)) {
|
if (isSubstringOfStrings([descendantCorpus], this.search)) {
|
||||||
// The descendant matches. We need to include *this* element,
|
// The descendant matches. We need to include *this* element,
|
||||||
// and all its *ancestors* in our list of narrowed results.
|
// and all its *ancestors* in our list of narrowed results.
|
||||||
const ascendants = this.$store.getters['element/getAncestors'](
|
const ascendants = this.$store.getters['element/getAncestors'](
|
||||||
this.page,
|
page,
|
||||||
descendant
|
descendant
|
||||||
)
|
)
|
||||||
filteredToElementIds.push(descendant.id)
|
filteredToElementIds.push(descendant.id)
|
||||||
|
@ -141,42 +247,15 @@ export default {
|
||||||
|
|
||||||
// Test of the root element itself matches the search query.
|
// Test of the root element itself matches the search query.
|
||||||
// if it does, it gets included in the narrowed results too.
|
// if it does, it gets included in the narrowed results too.
|
||||||
const rootCorpus = this.getElementCorpus(rootElement)
|
const rootCorpus = this.getElementCorpus(element, page)
|
||||||
if (isSubstringOfStrings([rootCorpus], this.search)) {
|
if (isSubstringOfStrings([rootCorpus], this.search)) {
|
||||||
// The root element matches.
|
// The root element matches.
|
||||||
filteredToElementIds.push(rootElement.id)
|
filteredToElementIds.push(element.id)
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
filteredToElementIds = [...new Set(filteredToElementIds)]
|
filteredToElementIds = [...new Set(filteredToElementIds)]
|
||||||
return filteredToElementIds
|
return filteredToElementIds
|
||||||
},
|
},
|
||||||
},
|
|
||||||
methods: {
|
|
||||||
...mapActions({
|
|
||||||
actionSelectElement: 'element/select',
|
|
||||||
}),
|
|
||||||
/*
|
|
||||||
* Given an element, this method will return the corpus we want to
|
|
||||||
* search against when a user enters a search query. Should we want
|
|
||||||
* to search both the 'display name' and the element type name, this
|
|
||||||
* method can be easily adapted to combine the two and return it.
|
|
||||||
*/
|
|
||||||
getElementCorpus(element) {
|
|
||||||
const elementType = this.$registry.get('element', element.type)
|
|
||||||
return elementType.getDisplayName(element, {
|
|
||||||
builder: this.builder,
|
|
||||||
page: this.page,
|
|
||||||
mode: this.mode,
|
|
||||||
element,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
onElementAdded() {
|
|
||||||
this.hide()
|
|
||||||
},
|
|
||||||
selectElement(element) {
|
|
||||||
this.actionSelectElement({ element })
|
|
||||||
this.hide()
|
|
||||||
},
|
|
||||||
shown() {
|
shown() {
|
||||||
this.search = null
|
this.search = null
|
||||||
this.$nextTick(() => {
|
this.$nextTick(() => {
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
<ul class="header__filter">
|
<ul class="header__filter">
|
||||||
<template v-for="actionType in pageActionTypes">
|
<template v-for="actionType in pageActionTypes">
|
||||||
<li
|
<li
|
||||||
v-if="actionType.isActive({ page, workspace })"
|
v-if="actionType.isActive({ page: currentPage, workspace })"
|
||||||
:key="actionType.getType()"
|
:key="actionType.getType()"
|
||||||
class="header__filter-item header__filter-item--right"
|
class="header__filter-item header__filter-item--right"
|
||||||
>
|
>
|
||||||
|
@ -14,7 +14,7 @@
|
||||||
component: $refs[`component_${actionType.type}`][0],
|
component: $refs[`component_${actionType.type}`][0],
|
||||||
button: $refs[`button_${actionType.type}`][0],
|
button: $refs[`button_${actionType.type}`][0],
|
||||||
builder: builder,
|
builder: builder,
|
||||||
page: page,
|
page: currentPage,
|
||||||
})
|
})
|
||||||
"
|
"
|
||||||
>
|
>
|
||||||
|
@ -29,7 +29,7 @@
|
||||||
:is="actionType.component"
|
:is="actionType.component"
|
||||||
:ref="`component_${actionType.type}`"
|
:ref="`component_${actionType.type}`"
|
||||||
:builder="builder"
|
:builder="builder"
|
||||||
:page="page"
|
:page="currentPage"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
</template>
|
</template>
|
||||||
|
@ -39,7 +39,7 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'PageActions',
|
name: 'PageActions',
|
||||||
inject: ['workspace', 'builder', 'page'],
|
inject: ['workspace', 'builder', 'currentPage'],
|
||||||
computed: {
|
computed: {
|
||||||
pageActionTypes() {
|
pageActionTypes() {
|
||||||
return Object.values(this.$registry.getOrderedList('pageAction'))
|
return Object.values(this.$registry.getOrderedList('pageAction'))
|
||||||
|
|
|
@ -22,12 +22,6 @@ export default {
|
||||||
DeviceSelector,
|
DeviceSelector,
|
||||||
PageActions,
|
PageActions,
|
||||||
},
|
},
|
||||||
props: {
|
|
||||||
page: {
|
|
||||||
type: Object,
|
|
||||||
required: true,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({ deviceTypeSelected: 'page/getDeviceTypeSelected' }),
|
...mapGetters({ deviceTypeSelected: 'page/getDeviceTypeSelected' }),
|
||||||
deviceTypes() {
|
deviceTypes() {
|
||||||
|
|
|
@ -23,7 +23,7 @@
|
||||||
:is="itemType.component"
|
:is="itemType.component"
|
||||||
:ref="`component_${itemType.type}`"
|
:ref="`component_${itemType.type}`"
|
||||||
:data-item-type="itemType.type"
|
:data-item-type="itemType.type"
|
||||||
:page="page"
|
:page="currentPage"
|
||||||
/>
|
/>
|
||||||
</li>
|
</li>
|
||||||
</ul>
|
</ul>
|
||||||
|
@ -32,7 +32,7 @@
|
||||||
<script>
|
<script>
|
||||||
export default {
|
export default {
|
||||||
name: 'PageHeaderMenuItems',
|
name: 'PageHeaderMenuItems',
|
||||||
inject: ['page'],
|
inject: ['currentPage'],
|
||||||
computed: {
|
computed: {
|
||||||
pageHeaderItemTypes() {
|
pageHeaderItemTypes() {
|
||||||
return this.$registry.getOrderedList('pageHeaderItem')
|
return this.$registry.getOrderedList('pageHeaderItem')
|
||||||
|
|
|
@ -8,12 +8,12 @@
|
||||||
</Alert>
|
</Alert>
|
||||||
<PageSettingsForm
|
<PageSettingsForm
|
||||||
:builder="builder"
|
:builder="builder"
|
||||||
:page="page"
|
:page="currentPage"
|
||||||
:default-values="page"
|
:default-values="currentPage"
|
||||||
@submitted="updatePage"
|
@submitted="updatePage"
|
||||||
>
|
>
|
||||||
<div
|
<div
|
||||||
v-if="$hasPermission('builder.page.update', page, workspace.id)"
|
v-if="$hasPermission('builder.page.update', currentPage, workspace.id)"
|
||||||
class="actions actions--right"
|
class="actions actions--right"
|
||||||
>
|
>
|
||||||
<Button size="large" :loading="loading" :disabled="loading">
|
<Button size="large" :loading="loading" :disabled="loading">
|
||||||
|
@ -34,7 +34,7 @@ export default {
|
||||||
name: 'PageSettings',
|
name: 'PageSettings',
|
||||||
components: { PageSettingsForm },
|
components: { PageSettingsForm },
|
||||||
mixins: [error],
|
mixins: [error],
|
||||||
inject: ['builder', 'page', 'workspace'],
|
inject: ['builder', 'currentPage', 'workspace'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
loading: false,
|
loading: false,
|
||||||
|
@ -50,7 +50,7 @@ export default {
|
||||||
try {
|
try {
|
||||||
await this.actionUpdatePage({
|
await this.actionUpdatePage({
|
||||||
builder: this.builder,
|
builder: this.builder,
|
||||||
page: this.page,
|
page: this.currentPage,
|
||||||
values: {
|
values: {
|
||||||
name,
|
name,
|
||||||
path,
|
path,
|
||||||
|
@ -60,7 +60,7 @@ export default {
|
||||||
await Promise.all(
|
await Promise.all(
|
||||||
pathPrams.map(({ name, type }) =>
|
pathPrams.map(({ name, type }) =>
|
||||||
this.$store.dispatch('pageParameter/setParameter', {
|
this.$store.dispatch('pageParameter/setParameter', {
|
||||||
page: this.page,
|
page: this.currentPage,
|
||||||
name,
|
name,
|
||||||
value: defaultValueForParameterType(type),
|
value: defaultValueForParameterType(type),
|
||||||
})
|
})
|
||||||
|
|
|
@ -62,8 +62,13 @@ export default {
|
||||||
PageSettingsNameFormElement,
|
PageSettingsNameFormElement,
|
||||||
},
|
},
|
||||||
mixins: [form],
|
mixins: [form],
|
||||||
inject: ['workspace', 'builder', 'page'],
|
inject: ['workspace', 'builder'],
|
||||||
props: {
|
props: {
|
||||||
|
page: {
|
||||||
|
type: Object,
|
||||||
|
required: false,
|
||||||
|
default: null,
|
||||||
|
},
|
||||||
isCreation: {
|
isCreation: {
|
||||||
type: Boolean,
|
type: Boolean,
|
||||||
required: false,
|
required: false,
|
||||||
|
|
|
@ -76,7 +76,7 @@
|
||||||
allowAllRolesExceptSelected ||
|
allowAllRolesExceptSelected ||
|
||||||
disallowAllRolesExceptSelected
|
disallowAllRolesExceptSelected
|
||||||
"
|
"
|
||||||
class="visibility-form__role-checkbox-container"
|
class="visibility-form__role-list"
|
||||||
>
|
>
|
||||||
<template v-if="loadingRoles">
|
<template v-if="loadingRoles">
|
||||||
<div class="loading margin-bottom-1"></div>
|
<div class="loading margin-bottom-1"></div>
|
||||||
|
@ -85,7 +85,7 @@
|
||||||
<div
|
<div
|
||||||
v-for="roleName in allRoles"
|
v-for="roleName in allRoles"
|
||||||
:key="roleName"
|
:key="roleName"
|
||||||
class="visibility-form__role-checkbox-div"
|
class="visibility-form__role-checkbox"
|
||||||
>
|
>
|
||||||
<Checkbox
|
<Checkbox
|
||||||
:checked="isChecked(roleName)"
|
:checked="isChecked(roleName)"
|
||||||
|
@ -95,14 +95,11 @@
|
||||||
</Checkbox>
|
</Checkbox>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="visibility-form__role-links">
|
<div class="visibility-form__actions">
|
||||||
<a @click.prevent="selectAllRoles">
|
<a @click.prevent="selectAllRoles">
|
||||||
{{ $t('visibilityForm.rolesSelectAll') }}
|
{{ $t('visibilityForm.rolesSelectAll') }}
|
||||||
</a>
|
</a>
|
||||||
<a
|
<a @click.prevent="deselectAllRoles">
|
||||||
class="visibility-form__role-links-deselect-all"
|
|
||||||
@click.prevent="deselectAllRoles"
|
|
||||||
>
|
|
||||||
{{ $t('visibilityForm.rolesDeselectAll') }}
|
{{ $t('visibilityForm.rolesDeselectAll') }}
|
||||||
</a>
|
</a>
|
||||||
</div>
|
</div>
|
||||||
|
@ -120,12 +117,13 @@
|
||||||
<script>
|
<script>
|
||||||
import { StoreItemLookupError } from '@baserow/modules/core/errors'
|
import { StoreItemLookupError } from '@baserow/modules/core/errors'
|
||||||
import visibilityForm from '@baserow/modules/builder/mixins/visibilityForm'
|
import visibilityForm from '@baserow/modules/builder/mixins/visibilityForm'
|
||||||
|
import form from '@baserow/modules/core/mixins/form'
|
||||||
|
|
||||||
import { VISIBILITY_LOGGED_IN } from '@baserow/modules/builder/constants'
|
import { VISIBILITY_LOGGED_IN } from '@baserow/modules/builder/constants'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'PageVisibilityForm',
|
name: 'PageVisibilityForm',
|
||||||
mixins: [visibilityForm],
|
mixins: [form, visibilityForm],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
values: {
|
values: {
|
||||||
|
|
|
@ -4,7 +4,7 @@
|
||||||
|
|
||||||
<div>
|
<div>
|
||||||
<PageVisibilityForm
|
<PageVisibilityForm
|
||||||
:default-values="page"
|
:default-values="currentPage"
|
||||||
@values-changed="updatePageVisibility"
|
@values-changed="updatePageVisibility"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
@ -20,7 +20,7 @@ export default {
|
||||||
name: 'PageVisibilitySettings',
|
name: 'PageVisibilitySettings',
|
||||||
components: { PageVisibilityForm },
|
components: { PageVisibilityForm },
|
||||||
mixins: [error],
|
mixins: [error],
|
||||||
inject: ['builder', 'page', 'workspace'],
|
inject: ['builder', 'currentPage', 'workspace'],
|
||||||
data() {
|
data() {
|
||||||
return {}
|
return {}
|
||||||
},
|
},
|
||||||
|
@ -31,7 +31,7 @@ export default {
|
||||||
try {
|
try {
|
||||||
await this.actionUpdatePage({
|
await this.actionUpdatePage({
|
||||||
builder: this.builder,
|
builder: this.builder,
|
||||||
page: this.page,
|
page: this.currentPage,
|
||||||
values,
|
values,
|
||||||
})
|
})
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|
|
@ -22,13 +22,8 @@ export default {
|
||||||
name: 'EventsSidePanel',
|
name: 'EventsSidePanel',
|
||||||
components: { Event },
|
components: { Event },
|
||||||
mixins: [elementSidePanel],
|
mixins: [elementSidePanel],
|
||||||
inject: ['applicationContext'],
|
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
applicationContext: {
|
|
||||||
...this.applicationContext,
|
|
||||||
element: this.element,
|
|
||||||
},
|
|
||||||
dataProvidersAllowed: DATA_PROVIDERS_ALLOWED_WORKFLOW_ACTIONS,
|
dataProvidersAllowed: DATA_PROVIDERS_ALLOWED_WORKFLOW_ACTIONS,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -38,7 +33,7 @@ export default {
|
||||||
},
|
},
|
||||||
workflowActions() {
|
workflowActions() {
|
||||||
return this.$store.getters['workflowAction/getElementWorkflowActions'](
|
return this.$store.getters['workflowAction/getElementWorkflowActions'](
|
||||||
this.page,
|
this.elementPage,
|
||||||
this.element.id
|
this.element.id
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
|
@ -16,13 +16,8 @@ import { DATA_PROVIDERS_ALLOWED_ELEMENTS } from '@baserow/modules/builder/enums'
|
||||||
export default {
|
export default {
|
||||||
name: 'GeneralSidePanel',
|
name: 'GeneralSidePanel',
|
||||||
mixins: [elementSidePanel],
|
mixins: [elementSidePanel],
|
||||||
inject: ['applicationContext'],
|
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
applicationContext: {
|
|
||||||
...this.applicationContext,
|
|
||||||
element: this.element,
|
|
||||||
},
|
|
||||||
dataProvidersAllowed: DATA_PROVIDERS_ALLOWED_ELEMENTS,
|
dataProvidersAllowed: DATA_PROVIDERS_ALLOWED_ELEMENTS,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -30,7 +30,6 @@ export default {
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
builder: this.builder,
|
builder: this.builder,
|
||||||
page: null,
|
|
||||||
mode: null,
|
mode: null,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
|
@ -209,7 +209,6 @@ export default {
|
||||||
try {
|
try {
|
||||||
await this.actionUpdateUserSource({
|
await this.actionUpdateUserSource({
|
||||||
application: this.builder,
|
application: this.builder,
|
||||||
page: this.page,
|
|
||||||
userSourceId: this.editedUserSource.id,
|
userSourceId: this.editedUserSource.id,
|
||||||
values: clone(newValues),
|
values: clone(newValues),
|
||||||
})
|
})
|
||||||
|
|
|
@ -7,14 +7,13 @@
|
||||||
class="margin-bottom-2"
|
class="margin-bottom-2"
|
||||||
>
|
>
|
||||||
<div class="control__elements">
|
<div class="control__elements">
|
||||||
<Dropdown v-model="values.data_source_id" :show-search="false">
|
<DataSourceDropdown
|
||||||
<DropdownItem
|
v-model="values.data_source_id"
|
||||||
v-for="dataSource in dataSources"
|
small
|
||||||
:key="dataSource.id"
|
:shared-data-sources="sharedDataSources"
|
||||||
:name="dataSource.name"
|
:local-data-sources="localDataSources"
|
||||||
:value="dataSource.id"
|
>
|
||||||
/>
|
</DataSourceDropdown>
|
||||||
</Dropdown>
|
|
||||||
</div>
|
</div>
|
||||||
</FormGroup>
|
</FormGroup>
|
||||||
</form>
|
</form>
|
||||||
|
@ -22,9 +21,11 @@
|
||||||
|
|
||||||
<script>
|
<script>
|
||||||
import elementForm from '@baserow/modules/builder/mixins/elementForm'
|
import elementForm from '@baserow/modules/builder/mixins/elementForm'
|
||||||
|
import DataSourceDropdown from '@baserow/modules/builder/components/dataSource/DataSourceDropdown'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
name: 'RefreshDataSourceWorkflowActionForm',
|
name: 'RefreshDataSourceWorkflowActionForm',
|
||||||
|
components: { DataSourceDropdown },
|
||||||
mixins: [elementForm],
|
mixins: [elementForm],
|
||||||
props: {
|
props: {
|
||||||
workflowAction: {
|
workflowAction: {
|
||||||
|
@ -42,8 +43,39 @@ export default {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
dataSources() {
|
sharedPage() {
|
||||||
return this.$store.getters['dataSource/getPageDataSources'](this.page)
|
return this.$store.getters['page/getSharedPage'](this.builder)
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Returns all data sources that are available not on shared page.
|
||||||
|
* @returns {Array} - The data sources the page designer can choose from.
|
||||||
|
*/
|
||||||
|
localDataSources() {
|
||||||
|
if (this.elementPage.id === this.sharedPage.id) {
|
||||||
|
// If the element is on the shared page they are no local page but only
|
||||||
|
// shared page.
|
||||||
|
return null
|
||||||
|
} else {
|
||||||
|
return this.$store.getters['dataSource/getPagesDataSources']([
|
||||||
|
this.elementPage,
|
||||||
|
]).filter((dataSource) => dataSource.type)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Returns the shared data sources.
|
||||||
|
* @returns {Array} - The shared data sources the page designer can choose from.
|
||||||
|
*/
|
||||||
|
sharedDataSources() {
|
||||||
|
// We keep only data sources that have a type and a schema.
|
||||||
|
return this.$store.getters['dataSource/getPagesDataSources']([
|
||||||
|
this.sharedPage,
|
||||||
|
]).filter(
|
||||||
|
(dataSource) =>
|
||||||
|
dataSource.type &&
|
||||||
|
this.$registry
|
||||||
|
.get('service', dataSource.type)
|
||||||
|
.getDataSchema(dataSource)
|
||||||
|
)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
364
web-frontend/modules/builder/elementTypeMixins.js
Normal file
364
web-frontend/modules/builder/elementTypeMixins.js
Normal file
|
@ -0,0 +1,364 @@
|
||||||
|
import { ELEMENT_EVENTS, SHARE_TYPES } from '@baserow/modules/builder/enums'
|
||||||
|
|
||||||
|
export const ContainerElementTypeMixin = (Base) =>
|
||||||
|
class extends Base {
|
||||||
|
isContainerElement = true
|
||||||
|
|
||||||
|
get elementTypesAll() {
|
||||||
|
return Object.values(this.app.$registry.getAll('element'))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Container element without any child elements is invalid. Return true
|
||||||
|
* if there are no children, otherwise return false.
|
||||||
|
*/
|
||||||
|
isInError({ page, element }) {
|
||||||
|
const children = this.app.store.getters['element/getChildren'](
|
||||||
|
page,
|
||||||
|
element
|
||||||
|
)
|
||||||
|
|
||||||
|
return !children.length
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const CollectionElementTypeMixin = (Base) =>
|
||||||
|
class extends Base {
|
||||||
|
isCollectionElement = true
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A helper function responsible for returning this collection element's
|
||||||
|
* schema properties.
|
||||||
|
*/
|
||||||
|
getSchemaProperties(dataSource) {
|
||||||
|
const serviceType = this.app.$registry.get('service', dataSource.type)
|
||||||
|
const schema = serviceType.getDataSchema(dataSource)
|
||||||
|
if (!schema) {
|
||||||
|
return []
|
||||||
|
}
|
||||||
|
return schema.type === 'array'
|
||||||
|
? schema.items.properties
|
||||||
|
: schema.properties
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Given a schema property name, is responsible for finding the matching
|
||||||
|
* property option in the element. If it doesn't exist, then we return
|
||||||
|
* an empty object, and it won't be included in the adhoc header.
|
||||||
|
* @param {object} element - the element we want to extract options from.
|
||||||
|
* @param {string} schemaProperty - the schema property name to check.
|
||||||
|
* @returns {object} - the matching property option, or an empty object.
|
||||||
|
*/
|
||||||
|
getPropertyOptionsByProperty(element, schemaProperty) {
|
||||||
|
return (
|
||||||
|
element.property_options.find((option) => {
|
||||||
|
return option.schema_property === schemaProperty
|
||||||
|
}) || {}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Responsible for iterating over the schema's properties, filtering
|
||||||
|
* the results down to the properties which are `filterable`, `sortable`,
|
||||||
|
* and `searchable`, and then returning the property value.
|
||||||
|
* @param {string} option - the `filterable`, `sortable` or `searchable`
|
||||||
|
* property option. If the value is `true` then the property will be
|
||||||
|
* included in the adhoc header component.
|
||||||
|
* @param {object} element - the element we want to extract options from.
|
||||||
|
* @param {object} dataSource - the dataSource used by `element`.
|
||||||
|
* @returns {array} - an array of schema properties which are present
|
||||||
|
* in the element's property options where `option` = `true`.
|
||||||
|
*/
|
||||||
|
getPropertyOptionByType(option, element, dataSource) {
|
||||||
|
const schemaProperties = dataSource
|
||||||
|
? this.getSchemaProperties(dataSource)
|
||||||
|
: []
|
||||||
|
return Object.entries(schemaProperties)
|
||||||
|
.filter(
|
||||||
|
([schemaProperty, _]) =>
|
||||||
|
this.getPropertyOptionsByProperty(element, schemaProperty)[
|
||||||
|
option
|
||||||
|
] || false
|
||||||
|
)
|
||||||
|
.map(([_, property]) => property)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of properties within this element which have been flagged
|
||||||
|
* as filterable by the page designer.
|
||||||
|
*/
|
||||||
|
adhocFilterableProperties(element, dataSource) {
|
||||||
|
return this.getPropertyOptionByType('filterable', element, dataSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of properties within this element which have been flagged
|
||||||
|
* as sortable by the page designer.
|
||||||
|
*/
|
||||||
|
adhocSortableProperties(element, dataSource) {
|
||||||
|
return this.getPropertyOptionByType('sortable', element, dataSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An array of properties within this element which have been flagged
|
||||||
|
* as searchable by the page designer.
|
||||||
|
*/
|
||||||
|
adhocSearchableProperties(element, dataSource) {
|
||||||
|
return this.getPropertyOptionByType('searchable', element, dataSource)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* By default collection element will load their content at loading time
|
||||||
|
* but if you don't want that you can return false here.
|
||||||
|
*/
|
||||||
|
get fetchAtLoad() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
hasCollectionAncestor(page, element) {
|
||||||
|
return this.app.store.getters['element/getAncestors'](page, element).some(
|
||||||
|
({ type }) => {
|
||||||
|
const ancestorType = this.app.$registry.get('element', type)
|
||||||
|
return ancestorType.isCollectionElement
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A simple check to return whether this collection element has a
|
||||||
|
* "source of data" (i.e. a data source, or a schema property).
|
||||||
|
* Should not be used as an "in error" or validation check, use
|
||||||
|
* `isInError` for this purpose as it is more thorough.
|
||||||
|
* @param element - The element we want to check for a source of data.
|
||||||
|
* @returns {Boolean} - Whether the element has a source of data.
|
||||||
|
*/
|
||||||
|
hasSourceOfData(element) {
|
||||||
|
return Boolean(element.data_source_id || element.schema_property)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Collection elements by default will have three permutations of display names:
|
||||||
|
*
|
||||||
|
* 1. If no data source exists, on `element` or its ancestors, then:
|
||||||
|
* - "Repeat" is returned.
|
||||||
|
* 2. If a data source is found, and `element` has no `schema_property`, then:
|
||||||
|
* - "Repeat {dataSourceName}" is returned.
|
||||||
|
* 3. If a data source is found, `element` has a `schema_property`, and the integration is Baserow, then:
|
||||||
|
* - "Repeat {schemaPropertyTitle} ({fieldTypeName})" is returned
|
||||||
|
* 4. If a data source is found, `element` has a `schema_property`, and the integration isn't Baserow, then:
|
||||||
|
* - "Repeat {schemaPropertyTitle}" is returned
|
||||||
|
* @param element - The element we want to get a display name for.
|
||||||
|
* @param page - The page the element belongs to.
|
||||||
|
* @returns {string} - The display name for the element.
|
||||||
|
*/
|
||||||
|
getDisplayName(element, { page, builder }) {
|
||||||
|
let suffix = ''
|
||||||
|
|
||||||
|
const collectionAncestors = this.app.store.getters[
|
||||||
|
'element/getAncestors'
|
||||||
|
](page, element, {
|
||||||
|
predicate: (ancestor) =>
|
||||||
|
this.app.$registry.get('element', ancestor.type)
|
||||||
|
.isCollectionElement && ancestor.data_source_id !== null,
|
||||||
|
})
|
||||||
|
|
||||||
|
// If the collection element has ancestors, pluck out the first one, which
|
||||||
|
// will have a data source. Otherwise, use `element`, as this element is
|
||||||
|
// the root level element.
|
||||||
|
const collectionElement = collectionAncestors.length
|
||||||
|
? collectionAncestors[0]
|
||||||
|
: element
|
||||||
|
|
||||||
|
// If we find a collection ancestor which has a data source, we'll
|
||||||
|
// use the data source's name as part of the display name.
|
||||||
|
if (collectionElement?.data_source_id) {
|
||||||
|
const sharedPage = this.app.store.getters['page/getSharedPage'](builder)
|
||||||
|
const dataSource = this.app.store.getters[
|
||||||
|
'dataSource/getPagesDataSourceById'
|
||||||
|
]([page, sharedPage], collectionElement?.data_source_id)
|
||||||
|
suffix = dataSource ? dataSource.name : ''
|
||||||
|
|
||||||
|
// If we have a data source, and the element has a schema property,
|
||||||
|
// we'll find the property within the data source's schema and pluck
|
||||||
|
// out the title property.
|
||||||
|
if (element.schema_property) {
|
||||||
|
// Find the schema properties. They'll be in different places,
|
||||||
|
// depending on whether this is a list or single row data source.
|
||||||
|
const schemaProperties =
|
||||||
|
dataSource.schema.type === 'array'
|
||||||
|
? dataSource.schema?.items?.properties
|
||||||
|
: dataSource.schema.properties
|
||||||
|
const schemaField = schemaProperties[element.schema_property]
|
||||||
|
// Only Local/Remote Baserow table schemas will have `original_type`,
|
||||||
|
// which is the `FieldType`. If we find it, we can use it to display
|
||||||
|
// what kind of field type was used.
|
||||||
|
suffix = schemaField?.title || element.schema_property
|
||||||
|
if (schemaField.original_type) {
|
||||||
|
const fieldType = this.app.$registry.get(
|
||||||
|
'field',
|
||||||
|
schemaField.original_type
|
||||||
|
)
|
||||||
|
suffix = `${suffix} (${fieldType.getName()})`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return suffix ? `${this.name} - ${suffix}` : this.name
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a data source is modified or destroyed, we need to ensure that
|
||||||
|
* our collection elements respond accordingly.
|
||||||
|
*
|
||||||
|
* If the data source has been removed, we want to remove it from the
|
||||||
|
* collection element, and then clear its contents from the store.
|
||||||
|
*
|
||||||
|
* If the data source has been updated, we want to trigger a content reset.
|
||||||
|
*
|
||||||
|
* @param event - `ELEMENT_EVENTS.DATA_SOURCE_REMOVED` if a data source
|
||||||
|
* has been destroyed, or `ELEMENT_EVENTS.DATA_SOURCE_AFTER_UPDATE` if
|
||||||
|
* it's been updated.
|
||||||
|
* @param params - Context data which the element type can use.
|
||||||
|
*/
|
||||||
|
async onElementEvent(event, { builder, element, dataSourceId }) {
|
||||||
|
const page = this.app.store.getters['page/getById'](
|
||||||
|
builder,
|
||||||
|
element.page_id
|
||||||
|
)
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A collection element is in error if:
|
||||||
|
*
|
||||||
|
* - No parent (including self) collection elements have a valid data_source_id.
|
||||||
|
* - The parent with the valid data_source_id points to a data_source
|
||||||
|
* that !returnsList and `schema_property` is blank.
|
||||||
|
* - It is nested in another collection element, and we don't have a `schema_property`.
|
||||||
|
* @param {Object} page - The page the repeat element belongs to.
|
||||||
|
* @param {Object} element - The repeat element
|
||||||
|
* @param {Object} builder - The builder
|
||||||
|
* @returns {Boolean} - Whether the element is in error.
|
||||||
|
*/
|
||||||
|
isInError({ page, element, builder }) {
|
||||||
|
// We get all parents with a valid data_source_id
|
||||||
|
const collectionAncestorsWithDataSource = this.app.store.getters[
|
||||||
|
'element/getAncestors'
|
||||||
|
](page, element, {
|
||||||
|
predicate: (ancestor) =>
|
||||||
|
this.app.$registry.get('element', ancestor.type)
|
||||||
|
.isCollectionElement && ancestor.data_source_id,
|
||||||
|
includeSelf: true,
|
||||||
|
})
|
||||||
|
|
||||||
|
// No parent with a data_source_id means we are in error
|
||||||
|
if (collectionAncestorsWithDataSource.length === 0) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// We consider the closest parent collection element with a data_source_id
|
||||||
|
// The closest parent might be the current element itself
|
||||||
|
const parentWithDataSource = collectionAncestorsWithDataSource.at(-1)
|
||||||
|
|
||||||
|
// We now check if the parent element configuration is correct.
|
||||||
|
const sharedPage = this.app.store.getters['page/getSharedPage'](builder)
|
||||||
|
const dataSource = this.app.store.getters[
|
||||||
|
'dataSource/getPagesDataSourceById'
|
||||||
|
]([page, sharedPage], parentWithDataSource.data_source_id)
|
||||||
|
|
||||||
|
// The data source is missing. May be it has been removed.
|
||||||
|
if (!dataSource) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
const serviceType = this.app.$registry.get('service', dataSource.type)
|
||||||
|
|
||||||
|
// If the data source type doesn't return a list, we should have a schema_property
|
||||||
|
if (!serviceType.returnsList && !parentWithDataSource.schema_property) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
// If the current element is not the one with the data source it should have
|
||||||
|
// a schema_property
|
||||||
|
if (parentWithDataSource.id !== element.id && !element.schema_property) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return super.isInError({ page, element, builder })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const MultiPageElementTypeMixin = (Base) =>
|
||||||
|
class extends Base {
|
||||||
|
isMultiPageElement = true
|
||||||
|
|
||||||
|
get onSharedPage() {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
isVisible({ element, currentPage }) {
|
||||||
|
if (!super.isVisible({ element, currentPage })) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
switch (element.share_type) {
|
||||||
|
case SHARE_TYPES.ALL:
|
||||||
|
return true
|
||||||
|
case SHARE_TYPES.ONLY:
|
||||||
|
return element.pages.includes(currentPage.id)
|
||||||
|
case SHARE_TYPES.EXCEPT:
|
||||||
|
return !element.pages.includes(currentPage.id)
|
||||||
|
default:
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
get childStylesForbidden() {
|
||||||
|
return ['style_width']
|
||||||
|
}
|
||||||
|
}
|
File diff suppressed because it is too large
Load diff
|
@ -12,7 +12,7 @@ import {
|
||||||
UserDataProviderType,
|
UserDataProviderType,
|
||||||
} from '@baserow/modules/builder/dataProviderTypes'
|
} from '@baserow/modules/builder/dataProviderTypes'
|
||||||
|
|
||||||
export const PLACEMENTS = {
|
export const DIRECTIONS = {
|
||||||
BEFORE: 'before',
|
BEFORE: 'before',
|
||||||
AFTER: 'after',
|
AFTER: 'after',
|
||||||
LEFT: 'left',
|
LEFT: 'left',
|
||||||
|
@ -83,6 +83,12 @@ export const BACKGROUND_MODES = {
|
||||||
FIT: 'fit',
|
FIT: 'fit',
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const PAGE_PLACES = {
|
||||||
|
HEADER: 'header',
|
||||||
|
CONTENT: 'content',
|
||||||
|
FOOTER: 'footer',
|
||||||
|
}
|
||||||
|
|
||||||
export const WIDTH_TYPES = {
|
export const WIDTH_TYPES = {
|
||||||
SMALL: { value: 'small', name: 'widthTypes.small' },
|
SMALL: { value: 'small', name: 'widthTypes.small' },
|
||||||
MEDIUM: { value: 'medium', name: 'widthTypes.medium' },
|
MEDIUM: { value: 'medium', name: 'widthTypes.medium' },
|
||||||
|
@ -91,6 +97,12 @@ export const WIDTH_TYPES = {
|
||||||
FULL_WIDTH: { value: 'full-width', name: 'widthTypes.fullWidth' },
|
FULL_WIDTH: { value: 'full-width', name: 'widthTypes.fullWidth' },
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const SHARE_TYPES = {
|
||||||
|
ALL: 'all',
|
||||||
|
ONLY: 'only',
|
||||||
|
EXCEPT: 'except',
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A list of all the data providers that can be used in the formula field on the right
|
* A list of all the data providers that can be used in the formula field on the right
|
||||||
* sidebar in the application builder.
|
* sidebar in the application builder.
|
||||||
|
|
|
@ -76,6 +76,7 @@
|
||||||
},
|
},
|
||||||
"elementsContext": {
|
"elementsContext": {
|
||||||
"searchPlaceholder": "Search elements",
|
"searchPlaceholder": "Search elements",
|
||||||
|
"noPageElements": "No elements found for this page",
|
||||||
"noElements": "No elements found"
|
"noElements": "No elements found"
|
||||||
},
|
},
|
||||||
"elementType": {
|
"elementType": {
|
||||||
|
@ -110,7 +111,18 @@
|
||||||
"recordSelector": "Record selector",
|
"recordSelector": "Record selector",
|
||||||
"recordSelectorDescription": "A related record selector",
|
"recordSelectorDescription": "A related record selector",
|
||||||
"dateTimePicker": "Date time picker",
|
"dateTimePicker": "Date time picker",
|
||||||
"dateTimePickerDescription": "A date and time input field"
|
"dateTimePickerDescription": "A date and time input field",
|
||||||
|
"header": "Multi-page header",
|
||||||
|
"headerDescription": "A container shared across pages",
|
||||||
|
"footer": "Multi-page footer",
|
||||||
|
"footerDescription": "A container shared across pages",
|
||||||
|
"notAllowedUnlessTop": "This element is allowed only at the top of the page",
|
||||||
|
"notAllowedUnlessBottom": "This element is allowed only at the bottom of the page",
|
||||||
|
"notAllowedUnlessHeader": "This element is allowed only inside the page header",
|
||||||
|
"notAllowedUnlessFooter": "This element is allowed only inside the page footer",
|
||||||
|
"notAllowedInsideContainer": "This element is not allowed inside a container",
|
||||||
|
"notAllowedInsideSameType": "This element is not allowed in a container of the same type",
|
||||||
|
"notAllowedLocation": "This element is not allowed at this location"
|
||||||
},
|
},
|
||||||
"addElementButton": {
|
"addElementButton": {
|
||||||
"label": "Element"
|
"label": "Element"
|
||||||
|
@ -118,7 +130,7 @@
|
||||||
"addElementModal": {
|
"addElementModal": {
|
||||||
"title": "Add new element",
|
"title": "Add new element",
|
||||||
"searchPlaceholder": "Search elements",
|
"searchPlaceholder": "Search elements",
|
||||||
"disabledElementTooltip": "Unavailable inside this element"
|
"elementInProgress": "Adding element..."
|
||||||
},
|
},
|
||||||
"elementMenu": {
|
"elementMenu": {
|
||||||
"moveUp": "Move up",
|
"moveUp": "Move up",
|
||||||
|
@ -142,7 +154,9 @@
|
||||||
"message": "Click on one of the elements to see more details"
|
"message": "Click on one of the elements to see more details"
|
||||||
},
|
},
|
||||||
"pagePreview": {
|
"pagePreview": {
|
||||||
"emptyMessage": "Click to create first element"
|
"emptyMessage": "Click to create an element",
|
||||||
|
"header": "HEADER",
|
||||||
|
"footer": "FOOTER"
|
||||||
},
|
},
|
||||||
"elementForms": {
|
"elementForms": {
|
||||||
"textInputPlaceholder": "Enter text...",
|
"textInputPlaceholder": "Enter text...",
|
||||||
|
@ -375,6 +389,9 @@
|
||||||
"textName": "Text",
|
"textName": "Text",
|
||||||
"numericName": "Numeric"
|
"numericName": "Numeric"
|
||||||
},
|
},
|
||||||
|
"pageEditor": {
|
||||||
|
"pageNotFound": "Page not found"
|
||||||
|
},
|
||||||
"publicPage": {
|
"publicPage": {
|
||||||
"siteNotFound": "Site not found",
|
"siteNotFound": "Site not found",
|
||||||
"pageNotFound": "Page not found"
|
"pageNotFound": "Page not found"
|
||||||
|
@ -842,7 +859,20 @@
|
||||||
"dataSourceDropdown": {
|
"dataSourceDropdown": {
|
||||||
"label": "Data source",
|
"label": "Data source",
|
||||||
"noDataSources": "No data sources available",
|
"noDataSources": "No data sources available",
|
||||||
|
"noSharedDataSources": "No shared data sources available",
|
||||||
"shared": "shared",
|
"shared": "shared",
|
||||||
"pageOnly": "this page"
|
"pageOnly": "this page"
|
||||||
|
},
|
||||||
|
"multiPageContainerElementForm": {
|
||||||
|
"pagePosition": "Position",
|
||||||
|
"behaviour": "Behaviour",
|
||||||
|
"display": "Display",
|
||||||
|
"selectAll": "Select all",
|
||||||
|
"deselectAll": "Deselect all"
|
||||||
|
},
|
||||||
|
"pageShareType": {
|
||||||
|
"all": "On all pages",
|
||||||
|
"only": "Only on selected pages",
|
||||||
|
"except": "Exclude selected pages"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,7 +34,7 @@ export default {
|
||||||
if (!this.element.data_source_id) {
|
if (!this.element.data_source_id) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const pages = [this.page, this.sharedPage]
|
const pages = [this.currentPage, this.sharedPage]
|
||||||
return this.getPagesDataSourceById(pages, this.element.data_source_id)
|
return this.getPagesDataSourceById(pages, this.element.data_source_id)
|
||||||
},
|
},
|
||||||
dataSourceType() {
|
dataSourceType() {
|
||||||
|
@ -50,10 +50,7 @@ export default {
|
||||||
return this.getHasMorePage(this.element)
|
return this.getHasMorePage(this.element)
|
||||||
},
|
},
|
||||||
contentLoading() {
|
contentLoading() {
|
||||||
return (
|
return this.getLoading(this.element) && !this.elementIsInError
|
||||||
this.$fetchState.pending ||
|
|
||||||
(this.getLoading(this.element) && !this.elementIsInError)
|
|
||||||
)
|
|
||||||
},
|
},
|
||||||
dispatchContext() {
|
dispatchContext() {
|
||||||
return DataProviderType.getAllDataSourceDispatchContext(
|
return DataProviderType.getAllDataSourceDispatchContext(
|
||||||
|
@ -73,7 +70,7 @@ export default {
|
||||||
},
|
},
|
||||||
elementIsInError() {
|
elementIsInError() {
|
||||||
return this.elementType.isInError({
|
return this.elementType.isInError({
|
||||||
page: this.page,
|
page: this.elementPage,
|
||||||
element: this.element,
|
element: this.element,
|
||||||
builder: this.builder,
|
builder: this.builder,
|
||||||
})
|
})
|
||||||
|
@ -112,7 +109,7 @@ export default {
|
||||||
},
|
},
|
||||||
async fetch() {
|
async fetch() {
|
||||||
if (!this.elementIsInError && this.elementType.fetchAtLoad) {
|
if (!this.elementIsInError && this.elementType.fetchAtLoad) {
|
||||||
await this.fetchContent([0, this.element.items_per_page], true)
|
await this.fetchContent([0, this.element.items_per_page])
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
methods: {
|
methods: {
|
||||||
|
@ -134,7 +131,7 @@ export default {
|
||||||
}
|
}
|
||||||
try {
|
try {
|
||||||
await this.fetchElementContent({
|
await this.fetchElementContent({
|
||||||
page: this.page,
|
page: this.elementPage,
|
||||||
element: this.element,
|
element: this.element,
|
||||||
dataSource: this.dataSource,
|
dataSource: this.dataSource,
|
||||||
data: this.dispatchContext,
|
data: this.dispatchContext,
|
||||||
|
|
|
@ -1,9 +1,10 @@
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import applicationContextMixin from '@baserow/modules/builder/mixins/applicationContext'
|
import applicationContextMixin from '@baserow/modules/builder/mixins/applicationContext'
|
||||||
import { CurrentRecordDataProviderType } from '@baserow/modules/builder/dataProviderTypes'
|
import { CurrentRecordDataProviderType } from '@baserow/modules/builder/dataProviderTypes'
|
||||||
|
import elementForm from '@baserow/modules/builder/mixins/elementForm'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [applicationContextMixin],
|
mixins: [elementForm, applicationContextMixin],
|
||||||
computed: {
|
computed: {
|
||||||
/**
|
/**
|
||||||
* Returns the schema which the service schema property selector
|
* Returns the schema which the service schema property selector
|
||||||
|
@ -38,7 +39,7 @@ export default {
|
||||||
hasCollectionAncestor() {
|
hasCollectionAncestor() {
|
||||||
const { element } = this.applicationContext
|
const { element } = this.applicationContext
|
||||||
const elementType = this.$registry.get('element', element.type)
|
const elementType = this.$registry.get('element', element.type)
|
||||||
return elementType.hasCollectionAncestor(this.page, element)
|
return elementType.hasCollectionAncestor(this.elementPage, element)
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* In collection element forms, the ability to configure property options
|
* In collection element forms, the ability to configure property options
|
||||||
|
@ -96,25 +97,47 @@ export default {
|
||||||
return this.$store.getters['page/getSharedPage'](this.builder)
|
return this.$store.getters['page/getSharedPage'](this.builder)
|
||||||
},
|
},
|
||||||
/**
|
/**
|
||||||
* Returns all data sources that are available to the current page.
|
* Returns all data sources that are available not on shared page.
|
||||||
* The data source will need a `type` and a valid schema.
|
* The data source will need a `type` and a valid schema.
|
||||||
* @returns {Array} - The data sources the page designer can choose from.
|
* @returns {Array} - The data sources the page designer can choose from.
|
||||||
*/
|
*/
|
||||||
dataSources() {
|
localDataSources() {
|
||||||
const pages = [this.sharedPage, this.page]
|
if (this.elementPage.id === this.sharedPage.id) {
|
||||||
return this.$store.getters['dataSource/getPagesDataSources'](
|
// If the element is on the shared page they are no local page but only
|
||||||
pages
|
// shared page.
|
||||||
).filter((dataSource) => {
|
return null
|
||||||
|
} else {
|
||||||
|
return this.$store.getters['dataSource/getPagesDataSources']([
|
||||||
|
this.elementPage,
|
||||||
|
]).filter((dataSource) => {
|
||||||
|
const serviceType =
|
||||||
|
dataSource.type && this.$registry.get('service', dataSource.type)
|
||||||
|
return serviceType?.getDataSchema(dataSource)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
},
|
||||||
|
/**
|
||||||
|
* Returns the shared data sources.
|
||||||
|
* @returns {Array} - The shared data sources the page designer can choose from.
|
||||||
|
*/
|
||||||
|
sharedDataSources() {
|
||||||
|
// We keep only data sources that have a type and a schema.
|
||||||
|
return this.$store.getters['dataSource/getPagesDataSources']([
|
||||||
|
this.sharedPage,
|
||||||
|
]).filter((dataSource) => {
|
||||||
const serviceType =
|
const serviceType =
|
||||||
dataSource.type && this.$registry.get('service', dataSource.type)
|
dataSource.type && this.$registry.get('service', dataSource.type)
|
||||||
return serviceType?.getDataSchema(dataSource)
|
return serviceType?.getDataSchema(dataSource)
|
||||||
})
|
})
|
||||||
},
|
},
|
||||||
|
dataSources() {
|
||||||
|
return [...(this.localDataSources || []), ...this.sharedDataSources]
|
||||||
|
},
|
||||||
selectedDataSource() {
|
selectedDataSource() {
|
||||||
if (!this.values.data_source_id) {
|
if (!this.values.data_source_id) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
const pages = [this.sharedPage, this.page]
|
const pages = [this.sharedPage, this.currentPage]
|
||||||
return this.$store.getters['dataSource/getPagesDataSourceById'](
|
return this.$store.getters['dataSource/getPagesDataSourceById'](
|
||||||
pages,
|
pages,
|
||||||
this.values.data_source_id
|
this.values.data_source_id
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { ThemeConfigBlockType } from '@baserow/modules/builder/themeConfigBlockT
|
||||||
import applicationContextMixin from '@baserow/modules/builder/mixins/applicationContext'
|
import applicationContextMixin from '@baserow/modules/builder/mixins/applicationContext'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['workspace', 'builder', 'page', 'mode'],
|
inject: ['workspace', 'builder', 'elementPage', 'mode'],
|
||||||
mixins: [element, applicationContextMixin],
|
mixins: [element, applicationContextMixin],
|
||||||
props: {
|
props: {
|
||||||
element: {
|
element: {
|
||||||
|
|
|
@ -1,21 +1,20 @@
|
||||||
import element from '@baserow/modules/builder/mixins/element'
|
import element from '@baserow/modules/builder/mixins/element'
|
||||||
import { mapGetters } from 'vuex'
|
import { mapGetters } from 'vuex'
|
||||||
import { PLACEMENTS } from '@baserow/modules/builder/enums'
|
import { DIRECTIONS } from '@baserow/modules/builder/enums'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [element],
|
mixins: [element],
|
||||||
props: {
|
|
||||||
children: {
|
|
||||||
type: Array,
|
|
||||||
required: false,
|
|
||||||
default: () => [],
|
|
||||||
},
|
|
||||||
},
|
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
elementSelected: 'element/getSelected',
|
elementSelected: 'element/getSelected',
|
||||||
}),
|
}),
|
||||||
PLACEMENTS: () => PLACEMENTS,
|
DIRECTIONS: () => DIRECTIONS,
|
||||||
|
children() {
|
||||||
|
return this.$store.getters['element/getChildren'](
|
||||||
|
this.elementPage,
|
||||||
|
this.element
|
||||||
|
)
|
||||||
|
},
|
||||||
elementSelectedId() {
|
elementSelectedId() {
|
||||||
return this.elementSelected?.id
|
return this.elementSelected?.id
|
||||||
},
|
},
|
||||||
|
|
|
@ -5,7 +5,7 @@ import applicationContextMixin from '@baserow/modules/builder/mixins/application
|
||||||
import { ThemeConfigBlockType } from '@baserow/modules/builder/themeConfigBlockTypes'
|
import { ThemeConfigBlockType } from '@baserow/modules/builder/themeConfigBlockTypes'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['workspace', 'builder', 'page', 'mode'],
|
inject: ['workspace', 'builder', 'currentPage', 'elementPage', 'mode'],
|
||||||
mixins: [applicationContextMixin],
|
mixins: [applicationContextMixin],
|
||||||
props: {
|
props: {
|
||||||
element: {
|
element: {
|
||||||
|
@ -17,7 +17,7 @@ export default {
|
||||||
workflowActionsInProgress() {
|
workflowActionsInProgress() {
|
||||||
const workflowActions = this.$store.getters[
|
const workflowActions = this.$store.getters[
|
||||||
'workflowAction/getElementWorkflowActions'
|
'workflowAction/getElementWorkflowActions'
|
||||||
](this.page, this.element.id)
|
](this.elementPage, this.element.id)
|
||||||
const { recordIndexPath } = this.applicationContext
|
const { recordIndexPath } = this.applicationContext
|
||||||
const dispatchedById = this.elementType.uniqueElementId(
|
const dispatchedById = this.elementType.uniqueElementId(
|
||||||
this.element,
|
this.element,
|
||||||
|
@ -38,7 +38,7 @@ export default {
|
||||||
},
|
},
|
||||||
elementIsInError() {
|
elementIsInError() {
|
||||||
return this.elementType.isInError({
|
return this.elementType.isInError({
|
||||||
page: this.page,
|
page: this.elementPage,
|
||||||
element: this.element,
|
element: this.element,
|
||||||
builder: this.builder,
|
builder: this.builder,
|
||||||
})
|
})
|
||||||
|
@ -96,7 +96,7 @@ export default {
|
||||||
|
|
||||||
const workflowActions = this.$store.getters[
|
const workflowActions = this.$store.getters[
|
||||||
'workflowAction/getElementWorkflowActions'
|
'workflowAction/getElementWorkflowActions'
|
||||||
](this.page, this.element.id).filter(
|
](this.elementPage, this.element.id).filter(
|
||||||
({ event: eventName }) => eventName === event.name
|
({ event: eventName }) => eventName === event.name
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
|
@ -3,7 +3,7 @@ import { ThemeConfigBlockType } from '@baserow/modules/builder/themeConfigBlockT
|
||||||
import form from '@baserow/modules/core/mixins/form'
|
import form from '@baserow/modules/core/mixins/form'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['workspace', 'builder', 'page', 'mode'],
|
inject: ['workspace', 'builder', 'currentPage', 'elementPage', 'mode'],
|
||||||
mixins: [form],
|
mixins: [form],
|
||||||
computed: {
|
computed: {
|
||||||
themeConfigBlocks() {
|
themeConfigBlocks() {
|
||||||
|
|
|
@ -5,7 +5,18 @@ import { clone } from '@baserow/modules/core/utils/object'
|
||||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['workspace', 'builder', 'page'],
|
inject: ['workspace', 'builder', 'applicationContext'],
|
||||||
|
provide() {
|
||||||
|
return {
|
||||||
|
applicationContext: {
|
||||||
|
...this.applicationContext,
|
||||||
|
element: this.element,
|
||||||
|
page: this.elementPage,
|
||||||
|
},
|
||||||
|
// We add the current element page
|
||||||
|
elementPage: this.elementPage,
|
||||||
|
}
|
||||||
|
},
|
||||||
computed: {
|
computed: {
|
||||||
...mapGetters({
|
...mapGetters({
|
||||||
element: 'element/getSelected',
|
element: 'element/getSelected',
|
||||||
|
@ -20,11 +31,19 @@ export default {
|
||||||
|
|
||||||
parentElement() {
|
parentElement() {
|
||||||
return this.$store.getters['element/getElementById'](
|
return this.$store.getters['element/getElementById'](
|
||||||
this.page,
|
this.elementPage,
|
||||||
this.element?.parent_element_id
|
this.element?.parent_element_id
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
|
||||||
|
elementPage() {
|
||||||
|
// We use the page from the element itself
|
||||||
|
return this.$store.getters['page/getById'](
|
||||||
|
this.builder,
|
||||||
|
this.element.page_id
|
||||||
|
)
|
||||||
|
},
|
||||||
|
|
||||||
defaultValues() {
|
defaultValues() {
|
||||||
return this.element
|
return this.element
|
||||||
},
|
},
|
||||||
|
@ -57,7 +76,7 @@ export default {
|
||||||
if (Object.keys(differences).length > 0) {
|
if (Object.keys(differences).length > 0) {
|
||||||
try {
|
try {
|
||||||
await this.actionDebouncedUpdateSelectedElement({
|
await this.actionDebouncedUpdateSelectedElement({
|
||||||
page: this.page,
|
page: this.elementPage,
|
||||||
// Here we clone the values to prevent
|
// Here we clone the values to prevent
|
||||||
// "modification outside of the store" error
|
// "modification outside of the store" error
|
||||||
values: clone(differences),
|
values: clone(differences),
|
||||||
|
|
|
@ -29,7 +29,7 @@ export default {
|
||||||
},
|
},
|
||||||
formElementData() {
|
formElementData() {
|
||||||
return this.$store.getters['formData/getElementFormEntry'](
|
return this.$store.getters['formData/getElementFormEntry'](
|
||||||
this.page,
|
this.elementPage,
|
||||||
this.uniqueElementId
|
this.uniqueElementId
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -38,7 +38,7 @@ export default {
|
||||||
},
|
},
|
||||||
formElementInvalid() {
|
formElementInvalid() {
|
||||||
return this.$store.getters['formData/getElementInvalid'](
|
return this.$store.getters['formData/getElementInvalid'](
|
||||||
this.page,
|
this.elementPage,
|
||||||
this.uniqueElementId
|
this.uniqueElementId
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -52,7 +52,7 @@ export default {
|
||||||
},
|
},
|
||||||
formElementTouched() {
|
formElementTouched() {
|
||||||
return this.$store.getters['formData/getElementTouched'](
|
return this.$store.getters['formData/getElementTouched'](
|
||||||
this.page,
|
this.elementPage,
|
||||||
this.uniqueElementId
|
this.uniqueElementId
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
@ -62,7 +62,7 @@ export default {
|
||||||
*/
|
*/
|
||||||
isDescendantOfFormContainer() {
|
isDescendantOfFormContainer() {
|
||||||
return this.$store.getters['element/getAncestors'](
|
return this.$store.getters['element/getAncestors'](
|
||||||
this.page,
|
this.elementPage,
|
||||||
this.element
|
this.element
|
||||||
).some(({ type }) => type === FormContainerElementType.getType())
|
).some(({ type }) => type === FormContainerElementType.getType())
|
||||||
},
|
},
|
||||||
|
@ -85,7 +85,7 @@ export default {
|
||||||
},
|
},
|
||||||
setFormData(value) {
|
setFormData(value) {
|
||||||
return this.actionSetFormData({
|
return this.actionSetFormData({
|
||||||
page: this.page,
|
page: this.elementPage,
|
||||||
uniqueElementId: this.uniqueElementId,
|
uniqueElementId: this.uniqueElementId,
|
||||||
payload: {
|
payload: {
|
||||||
value,
|
value,
|
||||||
|
@ -106,7 +106,7 @@ export default {
|
||||||
*/
|
*/
|
||||||
onFormElementTouch() {
|
onFormElementTouch() {
|
||||||
this.$store.dispatch('formData/setElementTouched', {
|
this.$store.dispatch('formData/setElementTouched', {
|
||||||
page: this.page,
|
page: this.elementPage,
|
||||||
wasTouched: true,
|
wasTouched: true,
|
||||||
uniqueElementId: this.uniqueElementId,
|
uniqueElementId: this.uniqueElementId,
|
||||||
})
|
})
|
||||||
|
|
|
@ -2,7 +2,7 @@ import elementForm from '@baserow/modules/builder/mixins/elementForm'
|
||||||
import { DATA_PROVIDERS_ALLOWED_FORM_ELEMENTS } from '@baserow/modules/builder/enums'
|
import { DATA_PROVIDERS_ALLOWED_FORM_ELEMENTS } from '@baserow/modules/builder/enums'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
inject: ['workspace', 'builder', 'page', 'mode'],
|
inject: ['workspace', 'builder', 'currentPage', 'elementPage', 'mode'],
|
||||||
mixins: [elementForm],
|
mixins: [elementForm],
|
||||||
provide() {
|
provide() {
|
||||||
return {
|
return {
|
||||||
|
|
|
@ -1,5 +1,4 @@
|
||||||
import UserSourceService from '@baserow/modules/core/services/userSource'
|
import UserSourceService from '@baserow/modules/core/services/userSource'
|
||||||
import elementForm from '@baserow/modules/builder/mixins/elementForm'
|
|
||||||
|
|
||||||
import {
|
import {
|
||||||
DEFAULT_USER_ROLE_PREFIX,
|
DEFAULT_USER_ROLE_PREFIX,
|
||||||
|
@ -12,7 +11,7 @@ import {
|
||||||
} from '@baserow/modules/builder/constants'
|
} from '@baserow/modules/builder/constants'
|
||||||
|
|
||||||
export default {
|
export default {
|
||||||
mixins: [elementForm],
|
inject: ['builder'],
|
||||||
data() {
|
data() {
|
||||||
return {
|
return {
|
||||||
allRoles: [],
|
allRoles: [],
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
<template>
|
<template>
|
||||||
<div class="page-editor">
|
<div class="page-editor">
|
||||||
<PageHeader :page="page" />
|
<PageHeader />
|
||||||
<div class="layout__col-2-2 page-editor__content">
|
<div class="layout__col-2-2 page-editor__content">
|
||||||
<div :style="{ width: `calc(100% - ${panelWidth}px)` }">
|
<div :style="{ width: `calc(100% - ${panelWidth}px)` }">
|
||||||
<PagePreview />
|
<PagePreview />
|
||||||
|
@ -34,7 +34,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
workspace: this.workspace,
|
workspace: this.workspace,
|
||||||
builder: this.builder,
|
builder: this.builder,
|
||||||
page: this.page,
|
currentPage: this.currentPage,
|
||||||
mode,
|
mode,
|
||||||
formulaComponent: ApplicationBuilderFormulaInput,
|
formulaComponent: ApplicationBuilderFormulaInput,
|
||||||
applicationContext: this.applicationContext,
|
applicationContext: this.applicationContext,
|
||||||
|
@ -92,7 +92,7 @@ export default {
|
||||||
next()
|
next()
|
||||||
},
|
},
|
||||||
layout: 'app',
|
layout: 'app',
|
||||||
async asyncData({ store, params, error, $registry }) {
|
async asyncData({ store, params, error, $registry, app }) {
|
||||||
const builderId = parseInt(params.builderId)
|
const builderId = parseInt(params.builderId)
|
||||||
const pageId = parseInt(params.pageId)
|
const pageId = parseInt(params.pageId)
|
||||||
|
|
||||||
|
@ -115,7 +115,14 @@ export default {
|
||||||
|
|
||||||
const page = store.getters['page/getById'](builder, pageId)
|
const page = store.getters['page/getById'](builder, pageId)
|
||||||
|
|
||||||
await builderApplicationType.loadExtraData(builder, page, mode)
|
if (page.shared) {
|
||||||
|
return error({
|
||||||
|
statusCode: 404,
|
||||||
|
message: app.i18n.t('pageEditor.pageNotFound'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
await builderApplicationType.loadExtraData(builder, mode)
|
||||||
|
|
||||||
await Promise.all([
|
await Promise.all([
|
||||||
store.dispatch('dataSource/fetch', {
|
store.dispatch('dataSource/fetch', {
|
||||||
|
@ -139,14 +146,17 @@ export default {
|
||||||
|
|
||||||
data.workspace = workspace
|
data.workspace = workspace
|
||||||
data.builder = builder
|
data.builder = builder
|
||||||
data.page = page
|
data.currentPage = page
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
// In case of a network error we want to fail hard.
|
// In case of a network error we want to fail hard.
|
||||||
if (e.response === undefined && !(e instanceof StoreItemLookupError)) {
|
if (e.response === undefined && !(e instanceof StoreItemLookupError)) {
|
||||||
throw e
|
throw e
|
||||||
}
|
}
|
||||||
|
|
||||||
return error({ statusCode: 404, message: 'page not found.' })
|
return error({
|
||||||
|
statusCode: 404,
|
||||||
|
message: app.i18n.t('pageEditor.pageNotFound'),
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
@ -155,12 +165,13 @@ export default {
|
||||||
applicationContext() {
|
applicationContext() {
|
||||||
return {
|
return {
|
||||||
builder: this.builder,
|
builder: this.builder,
|
||||||
page: this.page,
|
|
||||||
mode,
|
mode,
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
dataSources() {
|
dataSources() {
|
||||||
return this.$store.getters['dataSource/getPageDataSources'](this.page)
|
return this.$store.getters['dataSource/getPageDataSources'](
|
||||||
|
this.currentPage
|
||||||
|
)
|
||||||
},
|
},
|
||||||
sharedPage() {
|
sharedPage() {
|
||||||
return this.$store.getters['page/getSharedPage'](this.builder)
|
return this.$store.getters['page/getSharedPage'](this.builder)
|
||||||
|
@ -173,7 +184,7 @@ export default {
|
||||||
dispatchContext() {
|
dispatchContext() {
|
||||||
return DataProviderType.getAllDataSourceDispatchContext(
|
return DataProviderType.getAllDataSourceDispatchContext(
|
||||||
this.$registry.getAll('builderDataProvider'),
|
this.$registry.getAll('builderDataProvider'),
|
||||||
this.applicationContext
|
{ ...this.applicationContext, page: this.currentPage }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
// Separate dispatch context for application level shared data sources
|
// Separate dispatch context for application level shared data sources
|
||||||
|
@ -195,7 +206,7 @@ export default {
|
||||||
this.$store.dispatch(
|
this.$store.dispatch(
|
||||||
'dataSourceContent/debouncedFetchPageDataSourceContent',
|
'dataSourceContent/debouncedFetchPageDataSourceContent',
|
||||||
{
|
{
|
||||||
page: this.page,
|
page: this.currentPage,
|
||||||
data: this.dispatchContext,
|
data: this.dispatchContext,
|
||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
}
|
}
|
||||||
|
@ -227,7 +238,7 @@ export default {
|
||||||
this.$store.dispatch(
|
this.$store.dispatch(
|
||||||
'dataSourceContent/debouncedFetchPageDataSourceContent',
|
'dataSourceContent/debouncedFetchPageDataSourceContent',
|
||||||
{
|
{
|
||||||
page: this.page,
|
page: this.currentPage,
|
||||||
data: newDispatchContext,
|
data: newDispatchContext,
|
||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
}
|
}
|
||||||
|
|
|
@ -3,10 +3,10 @@
|
||||||
<Toasts></Toasts>
|
<Toasts></Toasts>
|
||||||
<PageContent
|
<PageContent
|
||||||
v-if="canViewPage"
|
v-if="canViewPage"
|
||||||
:page="page"
|
|
||||||
:path="path"
|
:path="path"
|
||||||
:params="params"
|
:params="params"
|
||||||
:elements="elements"
|
:elements="elements"
|
||||||
|
:shared-elements="sharedElements"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
@ -45,7 +45,7 @@ export default {
|
||||||
return {
|
return {
|
||||||
workspace: this.workspace,
|
workspace: this.workspace,
|
||||||
builder: this.builder,
|
builder: this.builder,
|
||||||
page: this.page,
|
currentPage: this.currentPage,
|
||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
formulaComponent: ApplicationBuilderFormulaInput,
|
formulaComponent: ApplicationBuilderFormulaInput,
|
||||||
applicationContext: this.applicationContext,
|
applicationContext: this.applicationContext,
|
||||||
|
@ -111,6 +111,12 @@ export default {
|
||||||
store.dispatch('dataSource/fetchPublished', {
|
store.dispatch('dataSource/fetchPublished', {
|
||||||
page: sharedPage,
|
page: sharedPage,
|
||||||
}),
|
}),
|
||||||
|
store.dispatch('element/fetchPublished', {
|
||||||
|
page: sharedPage,
|
||||||
|
}),
|
||||||
|
store.dispatch('workflowAction/fetchPublished', {
|
||||||
|
page: sharedPage,
|
||||||
|
}),
|
||||||
])
|
])
|
||||||
|
|
||||||
await DataProviderType.initOnceAll(
|
await DataProviderType.initOnceAll(
|
||||||
|
@ -170,6 +176,13 @@ export default {
|
||||||
}
|
}
|
||||||
|
|
||||||
const [pageFound, path, pageParamsValue] = found
|
const [pageFound, path, pageParamsValue] = found
|
||||||
|
// Handle 404
|
||||||
|
if (pageFound.shared) {
|
||||||
|
return error({
|
||||||
|
statusCode: 404,
|
||||||
|
message: app.i18n.t('publicPage.pageNotFound'),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const page = await store.getters['page/getById'](builder, pageFound.id)
|
const page = await store.getters['page/getById'](builder, pageFound.id)
|
||||||
|
|
||||||
|
@ -217,7 +230,7 @@ export default {
|
||||||
|
|
||||||
return {
|
return {
|
||||||
builder,
|
builder,
|
||||||
page,
|
currentPage: page,
|
||||||
path,
|
path,
|
||||||
params,
|
params,
|
||||||
mode,
|
mode,
|
||||||
|
@ -226,7 +239,7 @@ export default {
|
||||||
head() {
|
head() {
|
||||||
return {
|
return {
|
||||||
titleTemplate: '',
|
titleTemplate: '',
|
||||||
title: this.page.name,
|
title: this.currentPage.name,
|
||||||
bodyAttrs: {
|
bodyAttrs: {
|
||||||
class: 'public-page',
|
class: 'public-page',
|
||||||
},
|
},
|
||||||
|
@ -235,12 +248,11 @@ export default {
|
||||||
},
|
},
|
||||||
computed: {
|
computed: {
|
||||||
elements() {
|
elements() {
|
||||||
return this.$store.getters['element/getRootElements'](this.page)
|
return this.$store.getters['element/getRootElements'](this.currentPage)
|
||||||
},
|
},
|
||||||
applicationContext() {
|
applicationContext() {
|
||||||
return {
|
return {
|
||||||
builder: this.builder,
|
builder: this.builder,
|
||||||
page: this.page,
|
|
||||||
pageParamsValue: this.params,
|
pageParamsValue: this.params,
|
||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
}
|
}
|
||||||
|
@ -253,13 +265,13 @@ export default {
|
||||||
return userCanViewPage(
|
return userCanViewPage(
|
||||||
this.$store.getters['userSourceUser/getUser'](this.builder),
|
this.$store.getters['userSourceUser/getUser'](this.builder),
|
||||||
this.$store.getters['userSourceUser/isAuthenticated'](this.builder),
|
this.$store.getters['userSourceUser/isAuthenticated'](this.builder),
|
||||||
this.page
|
this.currentPage
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
dispatchContext() {
|
dispatchContext() {
|
||||||
return DataProviderType.getAllDataSourceDispatchContext(
|
return DataProviderType.getAllDataSourceDispatchContext(
|
||||||
this.$registry.getAll('builderDataProvider'),
|
this.$registry.getAll('builderDataProvider'),
|
||||||
this.applicationContext
|
{ ...this.applicationContext, page: this.currentPage }
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
// Separate dispatch context for application level data sources
|
// Separate dispatch context for application level data sources
|
||||||
|
@ -277,6 +289,9 @@ export default {
|
||||||
this.sharedPage
|
this.sharedPage
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
|
sharedElements() {
|
||||||
|
return this.$store.getters['element/getRootElements'](this.sharedPage)
|
||||||
|
},
|
||||||
isAuthenticated() {
|
isAuthenticated() {
|
||||||
return this.$store.getters['userSourceUser/isAuthenticated'](this.builder)
|
return this.$store.getters['userSourceUser/isAuthenticated'](this.builder)
|
||||||
},
|
},
|
||||||
|
@ -307,7 +322,7 @@ export default {
|
||||||
this.$store.dispatch(
|
this.$store.dispatch(
|
||||||
'dataSourceContent/debouncedFetchPageDataSourceContent',
|
'dataSourceContent/debouncedFetchPageDataSourceContent',
|
||||||
{
|
{
|
||||||
page: this.page,
|
page: this.currentPage,
|
||||||
data: newDispatchContext,
|
data: newDispatchContext,
|
||||||
mode: this.mode,
|
mode: this.mode,
|
||||||
}
|
}
|
||||||
|
@ -327,16 +342,27 @@ export default {
|
||||||
{
|
{
|
||||||
page: this.sharedPage,
|
page: this.sharedPage,
|
||||||
data: newDispatchContext,
|
data: newDispatchContext,
|
||||||
|
mode: this.mode,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
async isAuthenticated() {
|
async isAuthenticated() {
|
||||||
// When the user logs in or out, we need to refetch the elements and actions
|
// When the user login or logout, we need to refetch the elements and actions
|
||||||
// as they might have changed.
|
// as they might have changed
|
||||||
this.$store.dispatch('element/fetchPublished', { page: this.page })
|
await this.$store.dispatch('element/fetchPublished', {
|
||||||
this.$store.dispatch('workflowAction/fetchPublished', { page: this.page })
|
page: this.sharedPage,
|
||||||
|
})
|
||||||
|
await this.$store.dispatch('element/fetchPublished', {
|
||||||
|
page: this.currentPage,
|
||||||
|
})
|
||||||
|
await this.$store.dispatch('workflowAction/fetchPublished', {
|
||||||
|
page: this.currentPage,
|
||||||
|
})
|
||||||
|
await this.$store.dispatch('workflowAction/fetchPublished', {
|
||||||
|
page: this.sharedPage,
|
||||||
|
})
|
||||||
|
|
||||||
// If the user is on a hidden page, redirect them to the Login page if possible.
|
// If the user is on a hidden page, redirect them to the Login page if possible.
|
||||||
await this.maybeRedirectUserToLoginPage()
|
await this.maybeRedirectUserToLoginPage()
|
||||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Add table
Reference in a new issue