mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-07 06:15:36 +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:
|
||||
page = PageHandler().get_page(
|
||||
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?
|
||||
|
|
|
@ -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.registries import element_type_registry
|
||||
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.core.services.registries import service_type_registry
|
||||
from baserow.core.user_sources.models import UserSource
|
||||
|
@ -112,6 +113,7 @@ class PublicElementSerializer(serializers.ModelSerializer):
|
|||
"page_id",
|
||||
"type",
|
||||
"order",
|
||||
"page_id",
|
||||
"parent_element_id",
|
||||
"place_in_container",
|
||||
"visibility",
|
||||
|
@ -272,7 +274,7 @@ class PublicBuilderSerializer(serializers.ModelSerializer):
|
|||
: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
|
||||
|
||||
|
|
|
@ -133,6 +133,7 @@ class ElementsView(APIView):
|
|||
@map_exceptions(
|
||||
{
|
||||
PageDoesNotExist: ERROR_PAGE_DOES_NOT_EXIST,
|
||||
ElementNotInSamePage: ERROR_ELEMENT_NOT_IN_SAME_PAGE,
|
||||
}
|
||||
)
|
||||
@validate_body_custom_fields(
|
||||
|
|
|
@ -48,7 +48,7 @@ class BuilderSerializer(serializers.ModelSerializer):
|
|||
: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")
|
||||
request = self.context.get("request")
|
||||
|
|
|
@ -167,7 +167,12 @@ class BuilderApplicationType(ApplicationType):
|
|||
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 = [
|
||||
PageHandler().export_page(
|
||||
|
|
|
@ -175,7 +175,9 @@ class BuilderConfig(AppConfig):
|
|||
ChoiceElementType,
|
||||
ColumnElementType,
|
||||
DateTimePickerElementType,
|
||||
FooterElementType,
|
||||
FormContainerElementType,
|
||||
HeaderElementType,
|
||||
HeadingElementType,
|
||||
IFrameElementType,
|
||||
ImageElementType,
|
||||
|
@ -203,6 +205,8 @@ class BuilderConfig(AppConfig):
|
|||
element_type_registry.register(CheckboxElementType())
|
||||
element_type_registry.register(IFrameElementType())
|
||||
element_type_registry.register(DateTimePickerElementType())
|
||||
element_type_registry.register(HeaderElementType())
|
||||
element_type_registry.register(FooterElementType())
|
||||
|
||||
from .domains.domain_types import CustomDomainType, SubDomainType
|
||||
from .domains.registries import domain_type_registry
|
||||
|
|
|
@ -33,6 +33,7 @@ from baserow.contrib.builder.elements.mixins import (
|
|||
CollectionElementWithFieldsTypeMixin,
|
||||
ContainerElementTypeMixin,
|
||||
FormElementTypeMixin,
|
||||
MultiPageElementTypeMixin,
|
||||
)
|
||||
from baserow.contrib.builder.elements.models import (
|
||||
INPUT_TEXT_TYPES,
|
||||
|
@ -43,7 +44,9 @@ from baserow.contrib.builder.elements.models import (
|
|||
ColumnElement,
|
||||
DateTimePickerElement,
|
||||
Element,
|
||||
FooterElement,
|
||||
FormContainerElement,
|
||||
HeaderElement,
|
||||
HeadingElement,
|
||||
IFrameElement,
|
||||
ImageElement,
|
||||
|
@ -117,7 +120,7 @@ class ColumnElementType(ContainerElementTypeMixin, ElementType):
|
|||
type = "column"
|
||||
model_class = ColumnElement
|
||||
|
||||
class SerializedDict(ElementDict):
|
||||
class SerializedDict(ContainerElementTypeMixin.SerializedDict):
|
||||
column_amount: int
|
||||
column_gap: int
|
||||
alignment: str
|
||||
|
@ -191,8 +194,8 @@ class ColumnElementType(ContainerElementTypeMixin, ElementType):
|
|||
"""
|
||||
|
||||
return [
|
||||
element_type.type
|
||||
for element_type in element_type_registry.get_all()
|
||||
element_type
|
||||
for element_type in super().child_types_allowed
|
||||
if element_type.type != self.type
|
||||
]
|
||||
|
||||
|
@ -210,7 +213,7 @@ class FormContainerElementType(ContainerElementTypeMixin, ElementType):
|
|||
]
|
||||
simple_formula_fields = ["submit_button_label"]
|
||||
|
||||
class SerializedDict(ElementDict):
|
||||
class SerializedDict(ContainerElementTypeMixin.SerializedDict):
|
||||
submit_button_label: BaserowFormula
|
||||
reset_initial_values_post_submission: bool
|
||||
|
||||
|
@ -261,8 +264,8 @@ class FormContainerElementType(ContainerElementTypeMixin, ElementType):
|
|||
"""
|
||||
|
||||
return [
|
||||
element_type.type
|
||||
for element_type in element_type_registry.get_all()
|
||||
element_type
|
||||
for element_type in super().child_types_allowed
|
||||
if element_type.type != self.type
|
||||
]
|
||||
|
||||
|
@ -858,6 +861,16 @@ class NavigationElementManager:
|
|||
"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(
|
||||
self, values: Dict, instance: Optional[LinkElement] = None
|
||||
):
|
||||
|
@ -1939,3 +1952,35 @@ class DateTimePickerElementType(FormElementTypeMixin, ElementType):
|
|||
"include_time": False,
|
||||
"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,
|
||||
)
|
||||
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.database.fields.utils import get_field_id_from_field_key
|
||||
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.
|
||||
|
||||
: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(
|
||||
self, container_element: ContainerElement, places_removed: List[str]
|
||||
|
@ -128,6 +133,8 @@ class ContainerElementTypeMixin:
|
|||
:raises DRFValidationError: If the place in container is invalid
|
||||
"""
|
||||
|
||||
return True
|
||||
|
||||
|
||||
class CollectionElementTypeMixin:
|
||||
is_collection_element = True
|
||||
|
@ -738,3 +745,119 @@ class FormElementTypeMixin:
|
|||
)
|
||||
|
||||
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,
|
||||
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.mixins import BuilderInstanceWithFormulaMixin
|
||||
from baserow.contrib.builder.pages.models import Page
|
||||
from baserow.contrib.database.db.functions import RandomUUID
|
||||
from baserow.core.registry import (
|
||||
CustomFieldsInstanceMixin,
|
||||
|
@ -58,6 +59,9 @@ class ElementType(
|
|||
parent_property_name = "page"
|
||||
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`.
|
||||
# By default, the priority is `0`, the lowest value. If this property is
|
||||
# not overridden, then the instance is imported last.
|
||||
|
@ -80,25 +84,62 @@ class ElementType(
|
|||
parent_element_id = values.get(
|
||||
"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:
|
||||
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(
|
||||
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}"
|
||||
)
|
||||
|
||||
if place_in_container is not None:
|
||||
parent_element_type.validate_place_in_container(
|
||||
place_in_container, parent_element
|
||||
# If we have a parent, we validate the place is accepted by this container.
|
||||
parent_element.get_type().validate_place_in_container(
|
||||
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):
|
||||
"""
|
||||
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:
|
||||
base_queryset = Page.objects
|
||||
base_queryset = Page.objects_with_shared
|
||||
|
||||
try:
|
||||
return base_queryset.select_related("builder", "builder__workspace").get(
|
||||
id=page_id
|
||||
)
|
||||
return base_queryset.select_related("builder__workspace").get(id=page_id)
|
||||
except Page.DoesNotExist:
|
||||
raise PageDoesNotExist()
|
||||
|
||||
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
|
||||
)
|
||||
|
||||
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:
|
||||
"""
|
||||
Creates the shared page of the given builder.
|
||||
|
@ -153,7 +167,7 @@ class PageHandler:
|
|||
self.is_page_path_unique(
|
||||
page.builder,
|
||||
path,
|
||||
base_queryset=Page.objects.exclude(
|
||||
base_queryset=Page.objects_with_shared.exclude(
|
||||
id=page.id
|
||||
), # We don't want to conflict with the current page
|
||||
raises=True,
|
||||
|
@ -188,7 +202,7 @@ class PageHandler:
|
|||
"""
|
||||
|
||||
if base_qs is None:
|
||||
base_qs = Page.objects.filter(builder=builder, shared=False)
|
||||
base_qs = Page.objects.filter(builder=builder)
|
||||
|
||||
try:
|
||||
full_order = Page.order_objects(base_qs, order)
|
||||
|
@ -345,7 +359,7 @@ class PageHandler:
|
|||
: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)
|
||||
|
||||
|
|
|
@ -20,6 +20,16 @@ if typing.TYPE_CHECKING:
|
|||
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(
|
||||
HierarchicalModelMixin,
|
||||
TrashableModelMixin,
|
||||
|
@ -36,6 +46,9 @@ class Page(
|
|||
ALLOW_ALL_EXCEPT = "allow_all_except"
|
||||
DISALLOW_ALL_EXCEPT = "disallow_all_except"
|
||||
|
||||
objects = PageWithoutSharedManager()
|
||||
objects_with_shared = models.Manager()
|
||||
|
||||
builder = models.ForeignKey("builder.Builder", on_delete=models.CASCADE)
|
||||
order = models.PositiveIntegerField()
|
||||
name = models.CharField(max_length=255)
|
||||
|
|
|
@ -37,8 +37,7 @@ class PageService:
|
|||
:return: The model instance of the Page
|
||||
"""
|
||||
|
||||
base_queryset = Page.objects.select_related("builder", "builder__workspace")
|
||||
page = self.handler.get_page(page_id, base_queryset=base_queryset)
|
||||
page = self.handler.get_page(page_id)
|
||||
|
||||
CoreHandler().check_permissions(
|
||||
user,
|
||||
|
@ -148,7 +147,8 @@ class PageService:
|
|||
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,
|
||||
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(
|
||||
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)
|
||||
|
||||
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(
|
||||
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)
|
||||
|
||||
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.
|
||||
|
||||
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 == {
|
||||
"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.
|
||||
|
||||
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 == {
|
||||
"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):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
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})
|
||||
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):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
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)
|
||||
|
||||
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):
|
||||
user, token = data_fixture.create_user_and_token()
|
||||
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})
|
||||
response = api_client.delete(
|
||||
|
|
|
@ -174,7 +174,7 @@ def test_get_builder_application(api_client, data_fixture):
|
|||
"login_page_id": None,
|
||||
"pages": [
|
||||
{
|
||||
"id": application.page_set.get(shared=True).id,
|
||||
"id": application.shared_page.id,
|
||||
"builder_id": application.id,
|
||||
"order": 1,
|
||||
"name": "__shared__",
|
||||
|
@ -233,7 +233,7 @@ def test_list_builder_applications(api_client, data_fixture):
|
|||
"login_page_id": None,
|
||||
"pages": [
|
||||
{
|
||||
"id": application.page_set.get(shared=True).id,
|
||||
"id": application.shared_page.id,
|
||||
"builder_id": application.id,
|
||||
"order": 1,
|
||||
"name": "__shared__",
|
||||
|
|
|
@ -48,7 +48,7 @@ def test_validate_login_page_id_raises_error_if_shared_page(
|
|||
builder = builder_fixture["builder"]
|
||||
|
||||
# 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(
|
||||
reverse("api:applications:item", kwargs={"application_id": builder.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(
|
||||
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(
|
||||
user=user,
|
||||
|
|
|
@ -94,7 +94,7 @@ def test_get_data_sources(data_fixture, specific):
|
|||
@pytest.mark.django_db
|
||||
def test_get_data_sources_with_shared(data_fixture):
|
||||
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(
|
||||
page=page
|
||||
)
|
||||
|
|
|
@ -186,10 +186,7 @@ def test_domain_publishing(data_fixture):
|
|||
assert domain1.published_to is not 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.exclude(shared=True).first().element_set.count()
|
||||
== 2
|
||||
)
|
||||
assert domain1.published_to.page_set.first().element_set.count() == 2
|
||||
|
||||
assert Builder.objects.count() == 2
|
||||
|
||||
|
|
|
@ -1,6 +1,7 @@
|
|||
from decimal import Decimal
|
||||
|
||||
import pytest
|
||||
from rest_framework.exceptions import ValidationError as DRFValidationError
|
||||
|
||||
from baserow.contrib.builder.elements.element_types import (
|
||||
ColumnElementType,
|
||||
|
@ -27,9 +28,13 @@ def pytest_generate_tests(metafunc):
|
|||
@pytest.mark.django_db
|
||||
def test_create_element(data_fixture, element_type):
|
||||
page = data_fixture.create_builder_page()
|
||||
shared_page = page.builder.shared_page
|
||||
|
||||
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)
|
||||
|
||||
assert element.page.id == page.id
|
||||
|
@ -41,6 +46,34 @@ def test_create_element(data_fixture, element_type):
|
|||
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
|
||||
def test_get_element(data_fixture):
|
||||
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):
|
||||
user = data_fixture.create_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")
|
||||
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
|
||||
for element_type in element_type_registry.get_all()
|
||||
if element_type.type != FormContainerElementType.type
|
||||
and not element_type.is_multi_page_element
|
||||
],
|
||||
)
|
||||
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
|
||||
|
@ -1347,10 +1350,13 @@ def test_choice_element_integer_option_values(data_fixture):
|
|||
element_type.type
|
||||
for element_type in element_type_registry.get_all()
|
||||
if element_type.type != ColumnElementType.type
|
||||
and not element_type.is_multi_page_element
|
||||
],
|
||||
)
|
||||
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
|
||||
|
@ -1513,7 +1519,7 @@ def test_repeat_element_import_export(data_fixture):
|
|||
imported_field = imported_table.field_set.get()
|
||||
|
||||
# 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_root_repeat = imported_page.element_set.get(
|
||||
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,
|
||||
)
|
||||
imported_builder = imported_apps[-1]
|
||||
imported_element = (
|
||||
imported_builder.page_set.filter(shared=False)
|
||||
.first()
|
||||
.element_set.first()
|
||||
.specific
|
||||
)
|
||||
imported_element = imported_builder.page_set.first().element_set.first().specific
|
||||
|
||||
# Check that the formula for option name suffix was updated with the new mapping
|
||||
import_option_name_suffix = imported_element.option_name_suffix
|
||||
|
|
|
@ -94,7 +94,7 @@ def test_delete_page(data_fixture):
|
|||
@pytest.mark.django_db
|
||||
def test_delete_shared_page(data_fixture):
|
||||
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):
|
||||
PageHandler().delete_page(shared_page)
|
||||
|
@ -114,7 +114,7 @@ def test_update_page(data_fixture):
|
|||
@pytest.mark.django_db
|
||||
def test_update_shared_page(data_fixture):
|
||||
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):
|
||||
PageHandler().update_page(shared_page, name="new")
|
||||
|
@ -158,7 +158,7 @@ def test_order_pages(data_fixture):
|
|||
@pytest.mark.django_db
|
||||
def test_order_pages_page_not_in_builder(data_fixture):
|
||||
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_two = data_fixture.create_builder_page(builder=builder, order=2)
|
||||
|
||||
|
@ -189,7 +189,7 @@ def test_duplicate_page(data_fixture):
|
|||
@pytest.mark.django_db
|
||||
def test_duplicate_shared_page(data_fixture):
|
||||
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):
|
||||
PageHandler().duplicate_page(shared_page)
|
||||
|
|
|
@ -60,11 +60,11 @@ def test_builder_application_type_init_application(data_fixture):
|
|||
user = data_fixture.create_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)
|
||||
|
||||
assert Page.objects.count() == 3 # With demo data
|
||||
assert Page.objects.count() == 2 # With demo data
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
|
@ -119,7 +119,7 @@ def test_builder_application_export(data_fixture):
|
|||
user = data_fixture.create_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)
|
||||
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.page_set.exclude(shared=True).count() == 2
|
||||
assert builder.page_set.filter(shared=True).count() == 1
|
||||
assert builder.page_set.count() == 2
|
||||
# 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
|
||||
first_integration = builder.integrations.first().specific
|
||||
|
@ -1011,7 +1014,7 @@ def test_builder_application_import(data_fixture):
|
|||
|
||||
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 page2.element_set.count() == 1
|
||||
|
@ -1371,7 +1374,7 @@ def test_builder_application_imports_correct_default_roles(data_fixture):
|
|||
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]
|
||||
|
||||
# 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
|
||||
|
||||
# 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):
|
||||
# Default Role's User Source should have changed for new elements
|
||||
if role.startswith(prefix):
|
||||
|
@ -1532,5 +1535,5 @@ def test_ensure_new_element_roles_are_sanitized_during_import_for_roles(
|
|||
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
|
||||
|
|
|
@ -901,7 +901,7 @@ def test_get_builder_used_property_names_returns_merged_property_names_integrati
|
|||
integration = data_fixture.create_local_baserow_integration(
|
||||
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)
|
||||
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()
|
||||
data_fixture.create_template(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)
|
||||
element_2 = data_fixture.create_builder_text_element(page=page_2)
|
||||
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 = [
|
||||
(
|
||||
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],
|
||||
),
|
||||
(
|
||||
|
|
|
@ -603,7 +603,7 @@ def test_export_import_local_baserow_upsert_row_service(
|
|||
imported_table = imported_database.table_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_integration = imported_builder.integrations.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()
|
||||
|
||||
# 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_filters = [
|
||||
{"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 }) => {
|
||||
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 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()
|
||||
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()
|
||||
|
||||
url = reverse(
|
||||
|
|
|
@ -65,7 +65,7 @@ import { mapActions } from 'vuex'
|
|||
export default {
|
||||
name: 'AuthFormElement',
|
||||
mixins: [element, form, error],
|
||||
inject: ['page', 'builder'],
|
||||
inject: ['elementPage', 'builder'],
|
||||
props: {
|
||||
/**
|
||||
* @type {Object}
|
||||
|
@ -128,7 +128,7 @@ export default {
|
|||
if (!found) {
|
||||
// If the user_source has been removed we need to update the element
|
||||
this.actionForceUpdateElement({
|
||||
page: this.page,
|
||||
page: this.elementPage,
|
||||
element: this.element,
|
||||
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
|
||||
if (!builder._loadedOnce) {
|
||||
const sharedPage = store.getters['page/getSharedPage'](builder)
|
||||
await Promise.all([
|
||||
store.dispatch('userSource/fetch', {
|
||||
application: builder,
|
||||
|
@ -98,8 +99,12 @@ export class BuilderApplicationType extends ApplicationType {
|
|||
}),
|
||||
// Fetch shared data sources
|
||||
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
|
||||
|
|
|
@ -19,8 +19,8 @@ export default {
|
|||
components: { FormulaInputField },
|
||||
mixins: [applicationContext],
|
||||
inject: {
|
||||
page: {
|
||||
from: 'page',
|
||||
elementPage: {
|
||||
from: 'elementPage',
|
||||
},
|
||||
builder: {
|
||||
from: 'builder',
|
||||
|
@ -38,10 +38,12 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
dataSourceLoading() {
|
||||
return this.$store.getters['dataSource/getLoading'](this.page)
|
||||
return this.$store.getters['dataSource/getLoading'](this.elementPage)
|
||||
},
|
||||
dataSourceContentLoading() {
|
||||
return this.$store.getters['dataSourceContent/getLoading'](this.page)
|
||||
return this.$store.getters['dataSourceContent/getLoading'](
|
||||
this.elementPage
|
||||
)
|
||||
},
|
||||
dataProviders() {
|
||||
return this.dataProvidersAllowed.map((dataProviderName) =>
|
||||
|
|
|
@ -61,7 +61,16 @@ export default {
|
|||
components: { DataSourceForm },
|
||||
|
||||
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: {
|
||||
dataSourceId: { type: Number, required: false, default: null },
|
||||
},
|
||||
|
@ -74,7 +83,9 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
dataSources() {
|
||||
return this.$store.getters['dataSource/getPageDataSources'](this.page)
|
||||
return this.$store.getters['dataSource/getPageDataSources'](
|
||||
this.currentPage
|
||||
)
|
||||
},
|
||||
sharedPage() {
|
||||
return this.$store.getters['page/getSharedPage'](this.builder)
|
||||
|
@ -108,7 +119,7 @@ export default {
|
|||
// edited. Sometimes it's the shared page.
|
||||
dataSourcePage() {
|
||||
if (!this.dataSource) {
|
||||
return this.page
|
||||
return this.currentPage
|
||||
}
|
||||
return this.$store.getters['page/getById'](
|
||||
this.builder,
|
||||
|
@ -116,7 +127,11 @@ export default {
|
|||
)
|
||||
},
|
||||
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: {
|
||||
|
@ -145,7 +160,7 @@ export default {
|
|||
try {
|
||||
if (this.create) {
|
||||
const createdDataSource = await this.actionCreateDataSource({
|
||||
page: this.page,
|
||||
page: this.currentPage,
|
||||
values,
|
||||
})
|
||||
this.actualDataSourceId = createdDataSource.id
|
||||
|
|
|
@ -17,18 +17,24 @@
|
|||
:icon-tooltip="$t('dataSourceDropdown.shared')"
|
||||
>
|
||||
</DropdownItem>
|
||||
<DropdownItem
|
||||
v-for="dataSource in pageDataSources"
|
||||
:key="dataSource.id"
|
||||
:name="getDataSourceLabel(dataSource)"
|
||||
:value="dataSource.id"
|
||||
icon="iconoir-empty-page"
|
||||
:icon-tooltip="$t('dataSourceDropdown.pageOnly')"
|
||||
>
|
||||
</DropdownItem>
|
||||
<template v-if="localDataSources">
|
||||
<DropdownItem
|
||||
v-for="dataSource in localDataSources"
|
||||
:key="dataSource.id"
|
||||
:name="getDataSourceLabel(dataSource)"
|
||||
:value="dataSource.id"
|
||||
icon="iconoir-empty-page"
|
||||
:icon-tooltip="$t('dataSourceDropdown.pageOnly')"
|
||||
>
|
||||
</DropdownItem
|
||||
></template>
|
||||
<template #emptyState>
|
||||
<slot name="emptyState"
|
||||
>{{ $t('dataSourceDropdown.noDataSources') }}
|
||||
<slot name="emptyState">
|
||||
{{
|
||||
isOnSharedPage
|
||||
? $t('dataSourceDropdown.noSharedDataSources')
|
||||
: $t('dataSourceDropdown.noDataSources')
|
||||
}}
|
||||
</slot>
|
||||
</template>
|
||||
</Dropdown>
|
||||
|
@ -44,30 +50,24 @@ export default {
|
|||
required: false,
|
||||
default: null,
|
||||
},
|
||||
dataSources: {
|
||||
sharedDataSources: {
|
||||
type: Array,
|
||||
required: true,
|
||||
},
|
||||
localDataSources: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
small: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
page: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
pageDataSources() {
|
||||
return this.dataSources.filter(
|
||||
({ page_id: pageId }) => pageId === this.page.id
|
||||
)
|
||||
},
|
||||
sharedDataSources() {
|
||||
return this.dataSources.filter(
|
||||
({ page_id: pageId }) => pageId !== this.page.id
|
||||
)
|
||||
isOnSharedPage() {
|
||||
return this.localDataSources === null
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
<template>
|
||||
<div
|
||||
:key="elementType.name"
|
||||
v-tooltip="disallowedTypeForAncestry ? disabledElementMessage : null"
|
||||
v-tooltip="disabled ? disabledMessage : null"
|
||||
class="add-element-card"
|
||||
:class="{ 'add-element-card--disabled': disabled }"
|
||||
v-on="$listeners"
|
||||
|
@ -30,20 +30,15 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
parentType: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
loading: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
disallowedTypeForAncestry: {
|
||||
type: Boolean,
|
||||
disabledMessage: {
|
||||
type: String,
|
||||
required: false,
|
||||
default: false,
|
||||
default: '',
|
||||
},
|
||||
disabled: {
|
||||
type: Boolean,
|
||||
|
@ -51,10 +46,5 @@ export default {
|
|||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
disabledElementMessage() {
|
||||
return this.$t('addElementModal.disabledElementTooltip')
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -13,10 +13,9 @@
|
|||
v-for="elementType in elementTypes"
|
||||
:key="elementType.getType()"
|
||||
:element-type="elementType"
|
||||
:parent-type="parentElementType"
|
||||
:disallowed-type-for-ancestry="isDisallowedByParent(elementType)"
|
||||
:loading="addingElementType === elementType.getType()"
|
||||
:disabled="isCardDisabled(elementType)"
|
||||
:disabled="isElementTypeDisabled(elementType)"
|
||||
:disabled-message="getElementTypeDisabledMessage(elementType)"
|
||||
@click="addElement(elementType)"
|
||||
/>
|
||||
</div>
|
||||
|
@ -29,21 +28,18 @@ import AddElementCard from '@baserow/modules/builder/components/elements/AddElem
|
|||
import { isSubstringOfStrings } from '@baserow/modules/core/utils/string'
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import { mapActions } from 'vuex'
|
||||
import { PAGE_PLACES } from '../../enums'
|
||||
|
||||
export default {
|
||||
name: 'AddElementModal',
|
||||
components: { AddElementCard },
|
||||
mixins: [modal],
|
||||
inject: ['builder', 'currentPage'],
|
||||
props: {
|
||||
page: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
elementTypesAllowed: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
},
|
||||
data() {
|
||||
return {
|
||||
|
@ -51,6 +47,7 @@ export default {
|
|||
placeInContainer: null,
|
||||
beforeId: null,
|
||||
parentElementId: null,
|
||||
pagePlace: null,
|
||||
addingElementType: null,
|
||||
}
|
||||
},
|
||||
|
@ -64,57 +61,101 @@ export default {
|
|||
)
|
||||
)
|
||||
},
|
||||
parentElementType() {
|
||||
const parentElement = this.$store.getters['element/getElementById'](
|
||||
this.page,
|
||||
this.parentElementId
|
||||
)
|
||||
return parentElement
|
||||
? this.$registry.get('element', parentElement.type)
|
||||
: null
|
||||
sharedPage() {
|
||||
return this.$store.getters['page/getSharedPage'](this.builder)
|
||||
},
|
||||
parentElement() {
|
||||
if (this.parentElementId) {
|
||||
return this.$store.getters['element/getElementByIdInPages'](
|
||||
[this.currentPage, this.sharedPage],
|
||||
this.parentElementId
|
||||
)
|
||||
}
|
||||
return null
|
||||
},
|
||||
beforeElement() {
|
||||
if (this.beforeId) {
|
||||
return this.$store.getters['element/getElementByIdInPages'](
|
||||
[this.currentPage, this.sharedPage],
|
||||
this.beforeId
|
||||
)
|
||||
}
|
||||
return null
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
isDisallowedByParent(elementType) {
|
||||
return (
|
||||
this.elementTypesAllowed !== null &&
|
||||
!this.elementTypesAllowed.includes(elementType)
|
||||
)
|
||||
getElementTypeDisabledMessage(elementType) {
|
||||
if (elementType.getType() === this.addingElementType) {
|
||||
// This type is disabled while we add it.
|
||||
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) {
|
||||
const isAddingElementType =
|
||||
this.addingElementType !== null &&
|
||||
elementType.getType() === this.addingElementType
|
||||
return isAddingElementType || this.isDisallowedByParent(elementType)
|
||||
isElementTypeDisabled(elementType) {
|
||||
return !!this.getElementTypeDisabledMessage(elementType)
|
||||
},
|
||||
...mapActions({
|
||||
actionCreateElement: 'element/create',
|
||||
}),
|
||||
|
||||
show({ placeInContainer, beforeId, parentElementId } = {}, ...args) {
|
||||
show(
|
||||
{ placeInContainer, beforeId, parentElementId, pagePlace } = {},
|
||||
...args
|
||||
) {
|
||||
this.placeInContainer = placeInContainer
|
||||
this.beforeId = beforeId
|
||||
this.parentElementId = parentElementId
|
||||
this.pagePlace = pagePlace
|
||||
modal.methods.show.bind(this)(...args)
|
||||
},
|
||||
|
||||
async addElement(elementType) {
|
||||
if (this.isCardDisabled(elementType)) {
|
||||
if (this.isElementTypeDisabled(elementType)) {
|
||||
return false
|
||||
}
|
||||
this.addingElementType = elementType.getType()
|
||||
const configuration = this.parentElementId
|
||||
? {
|
||||
parent_element_id: this.parentElementId,
|
||||
place_in_container: this.placeInContainer,
|
||||
}
|
||||
: null
|
||||
|
||||
let beforeId = this.beforeId
|
||||
let destinationPage
|
||||
|
||||
if (this.parentElementId) {
|
||||
// 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 {
|
||||
await this.actionCreateElement({
|
||||
page: this.page,
|
||||
page: destinationPage,
|
||||
elementType: elementType.getType(),
|
||||
beforeId: this.beforeId,
|
||||
configuration,
|
||||
beforeId,
|
||||
values: {
|
||||
parent_element_id: this.parentElementId,
|
||||
place_in_container: this.placeInContainer,
|
||||
},
|
||||
})
|
||||
|
||||
this.$emit('element-added')
|
||||
|
|
|
@ -1,9 +1,12 @@
|
|||
<template>
|
||||
<div class="add-element-zone" @click="!disabled && $emit('add-element')">
|
||||
<div
|
||||
class="add-element-zone"
|
||||
:class="{ 'add-element-zone--disabled': disabled }"
|
||||
>
|
||||
<div
|
||||
v-tooltip="disabled ? tooltip : null"
|
||||
class="add-element-zone__content"
|
||||
:class="{ 'add-element-zone__button--disabled': disabled }"
|
||||
class="add-element-zone__button"
|
||||
@click="!disabled && $emit('add-element')"
|
||||
>
|
||||
<i class="iconoir-plus add-element-zone__icon"></i>
|
||||
</div>
|
||||
|
|
|
@ -20,69 +20,62 @@
|
|||
</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="isPlacementVisible(PLACEMENTS.LEFT)"
|
||||
v-if="isDirectionVisible(DIRECTIONS.LEFT)"
|
||||
class="element-preview__menu-item"
|
||||
:class="{ disabled: isPlacementDisabled(PLACEMENTS.LEFT) }"
|
||||
@click="
|
||||
!isPlacementDisabled(PLACEMENTS.LEFT) && $emit('move', PLACEMENTS.LEFT)
|
||||
"
|
||||
:class="{
|
||||
'element-preview__menu-item--disabled': !isAllowedDirection(
|
||||
DIRECTIONS.LEFT
|
||||
),
|
||||
}"
|
||||
@click="$emit('move', DIRECTIONS.LEFT)"
|
||||
>
|
||||
<i class="iconoir-nav-arrow-left"></i>
|
||||
<span
|
||||
v-if="!isPlacementDisabled(PLACEMENTS.LEFT)"
|
||||
class="element-preview__menu-item-description"
|
||||
>
|
||||
<span class="element-preview__menu-item-description">
|
||||
{{ $t('elementMenu.moveLeft') }}
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="isPlacementVisible(PLACEMENTS.RIGHT)"
|
||||
v-if="isDirectionVisible(DIRECTIONS.RIGHT)"
|
||||
class="element-preview__menu-item"
|
||||
:class="{ disabled: isPlacementDisabled(PLACEMENTS.RIGHT) }"
|
||||
@click="
|
||||
!isPlacementDisabled(PLACEMENTS.RIGHT) &&
|
||||
$emit('move', PLACEMENTS.RIGHT)
|
||||
"
|
||||
:class="{
|
||||
'element-preview__menu-item--disabled': !isAllowedDirection(
|
||||
DIRECTIONS.RIGHT
|
||||
),
|
||||
}"
|
||||
@click="$emit('move', DIRECTIONS.RIGHT)"
|
||||
>
|
||||
<i class="iconoir-nav-arrow-right"></i>
|
||||
<span
|
||||
v-if="!isPlacementDisabled(PLACEMENTS.RIGHT)"
|
||||
class="element-preview__menu-item-description"
|
||||
>
|
||||
<span class="element-preview__menu-item-description">
|
||||
{{ $t('elementMenu.moveRight') }}
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="isPlacementVisible(PLACEMENTS.BEFORE)"
|
||||
v-if="isDirectionVisible(DIRECTIONS.BEFORE)"
|
||||
class="element-preview__menu-item"
|
||||
:class="{ disabled: isPlacementDisabled(PLACEMENTS.BEFORE) }"
|
||||
@click="
|
||||
!isPlacementDisabled(PLACEMENTS.BEFORE) &&
|
||||
$emit('move', PLACEMENTS.BEFORE)
|
||||
"
|
||||
:class="{
|
||||
'element-preview__menu-item--disabled': !isAllowedDirection(
|
||||
DIRECTIONS.BEFORE
|
||||
),
|
||||
}"
|
||||
@click="$emit('move', DIRECTIONS.BEFORE)"
|
||||
>
|
||||
<i class="iconoir-nav-arrow-up"></i>
|
||||
<span
|
||||
v-if="!isPlacementDisabled(PLACEMENTS.BEFORE)"
|
||||
class="element-preview__menu-item-description"
|
||||
>
|
||||
<span class="element-preview__menu-item-description">
|
||||
{{ $t('elementMenu.moveUp') }}
|
||||
</span>
|
||||
</a>
|
||||
<a
|
||||
v-if="isPlacementVisible(PLACEMENTS.AFTER)"
|
||||
v-if="isDirectionVisible(DIRECTIONS.AFTER)"
|
||||
class="element-preview__menu-item"
|
||||
:class="{ disabled: isPlacementDisabled(PLACEMENTS.AFTER) }"
|
||||
@click="
|
||||
!isPlacementDisabled(PLACEMENTS.AFTER) &&
|
||||
$emit('move', PLACEMENTS.AFTER)
|
||||
"
|
||||
:class="{
|
||||
'element-preview__menu-item--disabled': !isAllowedDirection(
|
||||
DIRECTIONS.AFTER
|
||||
),
|
||||
}"
|
||||
@click="$emit('move', DIRECTIONS.AFTER)"
|
||||
>
|
||||
<i class="iconoir-nav-arrow-down"></i>
|
||||
<span
|
||||
v-if="!isPlacementDisabled(PLACEMENTS.AFTER)"
|
||||
class="element-preview__menu-item-description"
|
||||
>
|
||||
<span class="element-preview__menu-item-description">
|
||||
{{ $t('elementMenu.moveDown') }}
|
||||
</span>
|
||||
</a>
|
||||
|
@ -96,7 +89,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { PLACEMENTS } from '@baserow/modules/builder/enums'
|
||||
import { DIRECTIONS } from '@baserow/modules/builder/enums'
|
||||
|
||||
export default {
|
||||
name: 'ElementMenu',
|
||||
|
@ -111,26 +104,26 @@ export default {
|
|||
required: false,
|
||||
default: false,
|
||||
},
|
||||
placements: {
|
||||
directions: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [PLACEMENTS.BEFORE, PLACEMENTS.AFTER],
|
||||
default: () => [DIRECTIONS.BEFORE, DIRECTIONS.AFTER],
|
||||
},
|
||||
placementsDisabled: {
|
||||
allowedDirections: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
PLACEMENTS: () => PLACEMENTS,
|
||||
DIRECTIONS: () => DIRECTIONS,
|
||||
},
|
||||
methods: {
|
||||
isPlacementVisible(placement) {
|
||||
return this.placements.includes(placement)
|
||||
isDirectionVisible(direction) {
|
||||
return this.directions.includes(direction)
|
||||
},
|
||||
isPlacementDisabled(placement) {
|
||||
return this.placementsDisabled.includes(placement)
|
||||
isAllowedDirection(direction) {
|
||||
return this.allowedDirections.includes(direction)
|
||||
},
|
||||
},
|
||||
}
|
||||
|
|
|
@ -20,38 +20,37 @@
|
|||
v-show="isSelected"
|
||||
v-if="canCreate"
|
||||
class="element-preview__insert element-preview__insert--top"
|
||||
@click="showAddElementModal(PLACEMENTS.BEFORE)"
|
||||
@click="showAddElementModal(DIRECTIONS.BEFORE)"
|
||||
/>
|
||||
<ElementMenu
|
||||
v-if="isSelected && canUpdate"
|
||||
:placements="placements"
|
||||
:placements-disabled="placementsDisabled"
|
||||
:directions="directions"
|
||||
:allowed-directions="allowedMoveDirections"
|
||||
:is-duplicating="isDuplicating"
|
||||
:has-parent="!!parentElement"
|
||||
@delete="deleteElement"
|
||||
@move="$emit('move', $event)"
|
||||
@move="onMove"
|
||||
@duplicate="duplicateElement"
|
||||
@select-parent="selectParentElement()"
|
||||
/>
|
||||
|
||||
<PageElement
|
||||
:element="element"
|
||||
:mode="mode"
|
||||
class="element--read-only"
|
||||
:application-context-additions="applicationContextAdditions"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
|
||||
<InsertElementButton
|
||||
v-show="isSelected"
|
||||
v-if="canCreate"
|
||||
class="element-preview__insert element-preview__insert--bottom"
|
||||
@click="showAddElementModal(PLACEMENTS.AFTER)"
|
||||
@click="showAddElementModal(DIRECTIONS.AFTER)"
|
||||
/>
|
||||
<AddElementModal
|
||||
v-if="canCreate"
|
||||
ref="addElementModal"
|
||||
:element-types-allowed="elementTypesAllowed"
|
||||
:page="page"
|
||||
:page="elementPage"
|
||||
/>
|
||||
|
||||
<i
|
||||
|
@ -65,7 +64,7 @@
|
|||
import ElementMenu from '@baserow/modules/builder/components/elements/ElementMenu'
|
||||
import InsertElementButton from '@baserow/modules/builder/components/elements/InsertElementButton'
|
||||
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 { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
|
@ -85,27 +84,17 @@ export default {
|
|||
InsertElementButton,
|
||||
PageElement,
|
||||
},
|
||||
inject: ['workspace', 'builder', 'page', 'mode'],
|
||||
inject: ['workspace', 'builder', 'mode', 'currentPage'],
|
||||
props: {
|
||||
element: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
isLastElement: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isFirstElement: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
isRootElement: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
applicationContextAdditions: {
|
||||
type: Object,
|
||||
required: false,
|
||||
|
@ -124,7 +113,23 @@ export default {
|
|||
getClosestSiblingElement: 'element/getClosestSiblingElement',
|
||||
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() {
|
||||
if (
|
||||
!this.elementType.isVisible({
|
||||
element: this.element,
|
||||
currentPage: this.currentPage,
|
||||
})
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isAuthenticated = this.$store.getters[
|
||||
'userSourceUser/isAuthenticated'
|
||||
](this.builder)
|
||||
|
@ -151,13 +156,13 @@ export default {
|
|||
return true
|
||||
}
|
||||
},
|
||||
PLACEMENTS: () => PLACEMENTS,
|
||||
placements() {
|
||||
DIRECTIONS: () => DIRECTIONS,
|
||||
directions() {
|
||||
return [
|
||||
PLACEMENTS.BEFORE,
|
||||
PLACEMENTS.AFTER,
|
||||
PLACEMENTS.LEFT,
|
||||
PLACEMENTS.RIGHT,
|
||||
DIRECTIONS.BEFORE,
|
||||
DIRECTIONS.AFTER,
|
||||
DIRECTIONS.LEFT,
|
||||
DIRECTIONS.RIGHT,
|
||||
]
|
||||
},
|
||||
parentOfElementSelected() {
|
||||
|
@ -165,24 +170,34 @@ export default {
|
|||
return null
|
||||
}
|
||||
return this.$store.getters['element/getElementById'](
|
||||
this.page,
|
||||
this.elementPage,
|
||||
this.elementSelected.parent_element_id
|
||||
)
|
||||
},
|
||||
placementsDisabled() {
|
||||
const elementType = this.$registry.get('element', this.element.type)
|
||||
return elementType.getPlacementsDisabled(this.page, this.element)
|
||||
elementsAround() {
|
||||
return this.elementType.getElementsAround({
|
||||
builder: this.builder,
|
||||
page: this.currentPage,
|
||||
withSharedPage: true,
|
||||
element: this.element,
|
||||
})
|
||||
},
|
||||
elementTypesAllowed() {
|
||||
return (
|
||||
this.parentElementType?.childElementTypes(this.page, this.element) ||
|
||||
null
|
||||
)
|
||||
nextPlaces() {
|
||||
return this.elementType.getNextPlaces({
|
||||
builder: this.builder,
|
||||
page: this.elementPage,
|
||||
element: this.element,
|
||||
})
|
||||
},
|
||||
allowedMoveDirections() {
|
||||
return Object.entries(this.nextPlaces)
|
||||
.filter(([, nextPlace]) => !!nextPlace)
|
||||
.map(([direction]) => direction)
|
||||
},
|
||||
canCreate() {
|
||||
return this.$hasPermission(
|
||||
'builder.page.create_element',
|
||||
this.page,
|
||||
this.currentPage,
|
||||
this.workspace.id
|
||||
)
|
||||
},
|
||||
|
@ -200,7 +215,7 @@ export default {
|
|||
if (!this.elementSelected) {
|
||||
return []
|
||||
}
|
||||
return this.elementAncestors(this.page, this.elementSelected).map(
|
||||
return this.elementAncestors(this.elementPage, this.elementSelected).map(
|
||||
({ id }) => id
|
||||
)
|
||||
},
|
||||
|
@ -215,7 +230,7 @@ export default {
|
|||
return null
|
||||
}
|
||||
return this.$store.getters['element/getElementById'](
|
||||
this.page,
|
||||
this.elementPage,
|
||||
this.element.parent_element_id
|
||||
)
|
||||
},
|
||||
|
@ -224,15 +239,9 @@ export default {
|
|||
? this.$registry.get('element', this.parentElement?.type)
|
||||
: null
|
||||
},
|
||||
nextElement() {
|
||||
return this.$store.getters['element/getNextElement'](
|
||||
this.page,
|
||||
this.element
|
||||
)
|
||||
},
|
||||
inError() {
|
||||
return this.elementType.isInError({
|
||||
page: this.page,
|
||||
page: this.elementPage,
|
||||
element: this.element,
|
||||
builder: this.builder,
|
||||
})
|
||||
|
@ -284,6 +293,9 @@ export default {
|
|||
actionDeleteElement: 'element/delete',
|
||||
actionSelectElement: 'element/select',
|
||||
}),
|
||||
onMove(direction) {
|
||||
this.$emit('move', { element: this.element, direction })
|
||||
},
|
||||
onSelect($event) {
|
||||
// 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`,
|
||||
|
@ -300,23 +312,32 @@ export default {
|
|||
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({
|
||||
placeInContainer: this.element.place_in_container,
|
||||
parentElementId: this.element.parent_element_id,
|
||||
beforeId: this.getBeforeId(placement),
|
||||
beforeId: this.getBeforeId(direction),
|
||||
pagePlace,
|
||||
})
|
||||
},
|
||||
getBeforeId(placement) {
|
||||
return placement === PLACEMENTS.BEFORE
|
||||
getBeforeId(direction) {
|
||||
return direction === DIRECTIONS.BEFORE
|
||||
? this.element.id
|
||||
: this.nextElement?.id || null
|
||||
: this.elementsAround[DIRECTIONS.AFTER]?.id || null
|
||||
},
|
||||
async duplicateElement() {
|
||||
this.isDuplicating = true
|
||||
try {
|
||||
await this.actionDuplicateElement({
|
||||
page: this.page,
|
||||
page: this.elementPage,
|
||||
elementId: this.element.id,
|
||||
})
|
||||
} catch (error) {
|
||||
|
@ -326,12 +347,15 @@ export default {
|
|||
},
|
||||
async deleteElement() {
|
||||
try {
|
||||
const siblingElementToSelect = this.getClosestSiblingElement(
|
||||
this.page,
|
||||
this.elementSelected
|
||||
)
|
||||
const siblingElementToSelect =
|
||||
this.elementsAround[DIRECTIONS.AFTER] ||
|
||||
this.elementsAround[DIRECTIONS.BEFORE] ||
|
||||
this.elementsAround[DIRECTIONS.LEFT] ||
|
||||
this.elementsAround[DIRECTIONS.RIGHT] ||
|
||||
this.parentOfElementSelected
|
||||
|
||||
await this.actionDeleteElement({
|
||||
page: this.page,
|
||||
page: this.elementPage,
|
||||
elementId: this.element.id,
|
||||
})
|
||||
if (siblingElementToSelect?.id) {
|
||||
|
|
|
@ -1,8 +1,5 @@
|
|||
<template>
|
||||
<ul
|
||||
v-auto-overflow-scroll
|
||||
class="elements-list__items elements-list__items--no-max-height"
|
||||
>
|
||||
<ul class="elements-list">
|
||||
<ElementsListItem
|
||||
v-for="element in filteredElements"
|
||||
:key="element.id"
|
||||
|
@ -19,7 +16,6 @@ import ElementsListItem from '@baserow/modules/builder/components/elements/Eleme
|
|||
export default {
|
||||
name: 'ElementsList',
|
||||
components: { ElementsListItem },
|
||||
inject: ['page'],
|
||||
props: {
|
||||
elements: {
|
||||
type: Array,
|
||||
|
|
|
@ -1,15 +1,15 @@
|
|||
<template>
|
||||
<li :key="element.id" class="elements-list__item">
|
||||
<a
|
||||
class="elements-list__item-link"
|
||||
:class="{
|
||||
'elements-list__item-link--selected': element.id === elementSelectedId,
|
||||
}"
|
||||
@click="$emit('select', element)"
|
||||
>
|
||||
<span class="elements-list__item-name">
|
||||
<i :class="`${elementType.iconClass} elements-list__item-icon`"></i>
|
||||
<span class="elements-list__item-name-text">{{
|
||||
<li
|
||||
:key="element.id"
|
||||
class="elements-list-item"
|
||||
:class="{
|
||||
'elements-list-item--selected': element.id === elementSelectedId,
|
||||
}"
|
||||
>
|
||||
<a class="elements-list-item__link" @click="$emit('select', element)">
|
||||
<span class="elements-list-item__name">
|
||||
<i :class="`${elementType.iconClass} elements-list-item__icon`"></i>
|
||||
<span class="elements-list-item__name-text">{{
|
||||
elementType.getDisplayName(element, applicationContext)
|
||||
}}</span>
|
||||
</span>
|
||||
|
@ -33,7 +33,7 @@ export default {
|
|||
ElementsList: () =>
|
||||
import('@baserow/modules/builder/components/elements/ElementsList'),
|
||||
},
|
||||
inject: ['builder', 'page', 'mode'],
|
||||
inject: ['builder', 'mode'],
|
||||
props: {
|
||||
element: {
|
||||
type: Object,
|
||||
|
@ -55,8 +55,18 @@ export default {
|
|||
elementType() {
|
||||
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() {
|
||||
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`.
|
||||
|
@ -76,7 +86,7 @@ export default {
|
|||
applicationContext() {
|
||||
return {
|
||||
builder: this.builder,
|
||||
page: this.page,
|
||||
page: this.elementPage,
|
||||
mode: this.mode,
|
||||
element: this.element,
|
||||
}
|
||||
|
|
|
@ -75,7 +75,9 @@ export default {
|
|||
}
|
||||
if (this.target === 'self' && this.url.startsWith('/')) {
|
||||
event.preventDefault()
|
||||
this.$router.push(this.url)
|
||||
if (this.$route.path !== this.url) {
|
||||
this.$router.push(this.url)
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
|
|
@ -25,8 +25,13 @@
|
|||
</slot>
|
||||
</template>
|
||||
<template #empty-state>
|
||||
<div class="ab-table__empty-message">
|
||||
{{ emptyStateMessage }}
|
||||
<div class="ab-table__empty-state">
|
||||
<template v-if="contentLoading">
|
||||
<div class="loading-spinner" />
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ $t('abTable.empty') }}
|
||||
</template>
|
||||
</div>
|
||||
</template>
|
||||
</BaserowTable>
|
||||
|
|
|
@ -20,7 +20,7 @@
|
|||
|
||||
<script>
|
||||
export default {
|
||||
inject: ['builder', 'page'],
|
||||
inject: ['builder', 'currentPage'],
|
||||
props: {
|
||||
element: {
|
||||
type: Object,
|
||||
|
@ -33,7 +33,7 @@ export default {
|
|||
},
|
||||
dataSource() {
|
||||
return this.$store.getters['dataSource/getPagesDataSourceById'](
|
||||
[this.page, this.sharedPage],
|
||||
[this.currentPage, this.sharedPage],
|
||||
this.element.data_source_id
|
||||
)
|
||||
},
|
||||
|
|
|
@ -22,7 +22,7 @@
|
|||
v-if="mode === 'editing'"
|
||||
:element="childCurrent"
|
||||
:application-context-additions="applicationContextAdditions"
|
||||
@move="move(childCurrent, $event)"
|
||||
@move="$emit('move', $event)"
|
||||
></ElementPreview>
|
||||
<PageElement
|
||||
v-else
|
||||
|
@ -35,21 +35,21 @@
|
|||
<AddElementZone
|
||||
v-else-if="
|
||||
mode === 'editing' &&
|
||||
$hasPermission('builder.page.create_element', page, workspace.id)
|
||||
$hasPermission(
|
||||
'builder.page.create_element',
|
||||
elementPage,
|
||||
workspace.id
|
||||
)
|
||||
"
|
||||
:page="elementPage"
|
||||
@add-element="showAddElementModal(columnIndex)"
|
||||
/>
|
||||
</div>
|
||||
<AddElementModal
|
||||
ref="addElementModal"
|
||||
:page="page"
|
||||
:element-types-allowed="elementType.childElementTypes(page, element)"
|
||||
/>
|
||||
<AddElementModal ref="addElementModal" :page="elementPage" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex'
|
||||
import _ from 'lodash'
|
||||
|
||||
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 ElementPreview from '@baserow/modules/builder/components/elements/ElementPreview'
|
||||
import { VERTICAL_ALIGNMENTS } from '@baserow/modules/builder/enums'
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
import { dimensionMixin } from '@baserow/modules/core/mixins/dimensions'
|
||||
|
||||
export default {
|
||||
|
@ -136,26 +135,12 @@ export default {
|
|||
this.dimensions.targetElement = this.$el.parentElement
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
actionMoveElement: 'element/moveElement',
|
||||
}),
|
||||
showAddElementModal(columnIndex) {
|
||||
this.$refs.addElementModal.show({
|
||||
placeInContainer: `${columnIndex}`,
|
||||
parentElementId: this.element.id,
|
||||
})
|
||||
},
|
||||
async move(element, placement) {
|
||||
try {
|
||||
await this.actionMoveElement({
|
||||
page: this.page,
|
||||
element,
|
||||
placement,
|
||||
})
|
||||
} catch (error) {
|
||||
notifyIf(error)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -4,14 +4,13 @@
|
|||
v-if="
|
||||
mode === 'editing' &&
|
||||
children.length === 0 &&
|
||||
$hasPermission('builder.page.create_element', page, workspace.id)
|
||||
$hasPermission('builder.page.create_element', currentPage, workspace.id)
|
||||
"
|
||||
>
|
||||
<AddElementZone @add-element="showAddElementModal"></AddElementZone>
|
||||
<AddElementModal
|
||||
ref="addElementModal"
|
||||
:page="page"
|
||||
:element-types-allowed="elementType.childElementTypes(page, element)"
|
||||
:page="elementPage"
|
||||
></AddElementModal>
|
||||
</div>
|
||||
<div v-else>
|
||||
|
@ -20,7 +19,7 @@
|
|||
v-if="mode === 'editing'"
|
||||
:key="child.id"
|
||||
:element="child"
|
||||
@move="moveElement(child, $event)"
|
||||
@move="$emit('move', $event)"
|
||||
/>
|
||||
<PageElement
|
||||
v-else
|
||||
|
@ -42,15 +41,12 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions } from 'vuex'
|
||||
|
||||
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'
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
|
||||
export default {
|
||||
name: 'FormContainerElement',
|
||||
|
@ -64,7 +60,6 @@ export default {
|
|||
props: {
|
||||
/**
|
||||
* @type {Object}
|
||||
* @property button_color - The submit button's color.
|
||||
* @property submit_button_label - The label of the submit button
|
||||
* @property reset_initial_values_post_submission - Whether to reset the form
|
||||
* elements to their initial value or not, following a successful submission.
|
||||
|
@ -83,7 +78,7 @@ export default {
|
|||
},
|
||||
getFormElementDescendants() {
|
||||
const descendants = this.$store.getters['element/getDescendants'](
|
||||
this.page,
|
||||
this.elementPage,
|
||||
this.element
|
||||
)
|
||||
return descendants
|
||||
|
@ -107,7 +102,7 @@ export default {
|
|||
recordIndexPath
|
||||
)
|
||||
return this.$store.getters['formData/getElementInvalid'](
|
||||
this.page,
|
||||
this.elementPage,
|
||||
uniqueElementId
|
||||
)
|
||||
}
|
||||
|
@ -115,9 +110,6 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
actionMoveElement: 'element/moveElement',
|
||||
}),
|
||||
/*
|
||||
* Responsible for marking all form element descendents in this form container
|
||||
* as touched, or not touched, depending on what we're achieving in validation.
|
||||
|
@ -131,7 +123,7 @@ export default {
|
|||
recordIndexPath
|
||||
)
|
||||
this.$store.dispatch('formData/setElementTouched', {
|
||||
page: this.page,
|
||||
page: this.elementPage,
|
||||
wasTouched,
|
||||
uniqueElementId,
|
||||
})
|
||||
|
@ -169,7 +161,7 @@ export default {
|
|||
),
|
||||
}
|
||||
this.$store.dispatch('formData/setFormData', {
|
||||
page: this.page,
|
||||
page: this.elementPage,
|
||||
payload,
|
||||
uniqueElementId,
|
||||
})
|
||||
|
@ -197,17 +189,6 @@ export default {
|
|||
parentElementId: this.element.id,
|
||||
})
|
||||
},
|
||||
async moveElement(element, placement) {
|
||||
try {
|
||||
await this.actionMoveElement({
|
||||
page: this.page,
|
||||
element,
|
||||
placement,
|
||||
})
|
||||
} catch (error) {
|
||||
notifyIf(error)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</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,
|
||||
],
|
||||
}"
|
||||
@move="moveElement(child, $event)"
|
||||
@move="$emit('move', $event)"
|
||||
/>
|
||||
<!-- Other iterations are not editable -->
|
||||
<!-- Override the mode so that any children are in public mode -->
|
||||
|
@ -68,10 +68,7 @@
|
|||
></AddElementZone>
|
||||
<AddElementModal
|
||||
ref="addElementModal"
|
||||
:page="page"
|
||||
:element-types-allowed="
|
||||
elementType.childElementTypes(page, element)
|
||||
"
|
||||
:page="elementPage"
|
||||
></AddElementModal>
|
||||
</template>
|
||||
</template>
|
||||
|
@ -92,10 +89,7 @@
|
|||
></AddElementZone>
|
||||
<AddElementModal
|
||||
ref="addElementModal"
|
||||
:page="page"
|
||||
:element-types-allowed="
|
||||
elementType.childElementTypes(page, element)
|
||||
"
|
||||
:page="elementPage"
|
||||
></AddElementModal>
|
||||
</template>
|
||||
<!-- We have no contents, but we do have children in edit mode -->
|
||||
|
@ -106,7 +100,7 @@
|
|||
v-for="child in children"
|
||||
:key="child.id"
|
||||
:element="child"
|
||||
@move="moveElement(child, $event)"
|
||||
@move="$emit('move', $event)"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
|
@ -127,7 +121,7 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
import { mapGetters } from 'vuex'
|
||||
|
||||
import AddElementZone from '@baserow/modules/builder/components/elements/AddElementZone'
|
||||
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 ElementPreview from '@baserow/modules/builder/components/elements/ElementPreview'
|
||||
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 { RepeatElementType } from '@baserow/modules/builder/elementTypes'
|
||||
import CollectionElementHeader from '@baserow/modules/builder/components/elements/components/CollectionElementHeader'
|
||||
|
@ -173,7 +166,7 @@ export default {
|
|||
},
|
||||
repeatElementIsNested() {
|
||||
return this.elementType.hasAncestorOfType(
|
||||
this.page,
|
||||
this.elementPage,
|
||||
this.element,
|
||||
RepeatElementType.getType()
|
||||
)
|
||||
|
@ -212,26 +205,12 @@ export default {
|
|||
},
|
||||
},
|
||||
methods: {
|
||||
...mapActions({
|
||||
actionMoveElement: 'element/moveElement',
|
||||
}),
|
||||
showAddElementModal() {
|
||||
this.$refs.addElementModal.show({
|
||||
placeInContainer: null,
|
||||
parentElementId: this.element.id,
|
||||
})
|
||||
},
|
||||
async moveElement(element, placement) {
|
||||
try {
|
||||
await this.actionMoveElement({
|
||||
page: this.page,
|
||||
element,
|
||||
placement,
|
||||
})
|
||||
} catch (error) {
|
||||
notifyIf(error)
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -31,7 +31,7 @@ export default {
|
|||
)
|
||||
const workflowActions = this.$store.getters[
|
||||
'workflowAction/getElementWorkflowActions'
|
||||
](this.page, this.element.id)
|
||||
](this.elementPage, this.element.id)
|
||||
return workflowActions
|
||||
.filter((wa) => wa.event === this.eventName)
|
||||
.some((workflowAction) =>
|
||||
|
|
|
@ -31,7 +31,7 @@
|
|||
|
||||
<div
|
||||
v-if="allowAllRolesExceptSelected || disallowAllRolesExceptSelected"
|
||||
class="visibility-form__role-checkbox-container"
|
||||
class="visibility-form__role-list"
|
||||
>
|
||||
<template v-if="loadingRoles">
|
||||
<div class="loading margin-bottom-1"></div>
|
||||
|
@ -40,7 +40,7 @@
|
|||
<div
|
||||
v-for="roleName in allRoles"
|
||||
:key="roleName"
|
||||
class="visibility-form__role-checkbox-div"
|
||||
class="visibility-form__role-checkbox"
|
||||
>
|
||||
<Checkbox
|
||||
:checked="isChecked(roleName)"
|
||||
|
@ -50,14 +50,11 @@
|
|||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<div class="visibility-form__role-links">
|
||||
<div class="visibility-form__actions">
|
||||
<a @click.prevent="selectAllRoles">
|
||||
{{ $t('visibilityForm.rolesSelectAll') }}
|
||||
</a>
|
||||
<a
|
||||
class="visibility-form__role-links-deselect-all"
|
||||
@click.prevent="deselectAllRoles"
|
||||
>
|
||||
<a @click.prevent="deselectAllRoles">
|
||||
{{ $t('visibilityForm.rolesDeselectAll') }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -79,6 +76,7 @@
|
|||
|
||||
<script>
|
||||
import visibilityForm from '@baserow/modules/builder/mixins/visibilityForm'
|
||||
import elementForm from '@baserow/modules/builder/mixins/elementForm'
|
||||
|
||||
import {
|
||||
VISIBILITY_ALL,
|
||||
|
@ -87,7 +85,7 @@ import {
|
|||
|
||||
export default {
|
||||
name: 'VisibilityForm',
|
||||
mixins: [visibilityForm],
|
||||
mixins: [elementForm, visibilityForm],
|
||||
data() {
|
||||
return {
|
||||
values: {
|
||||
|
|
|
@ -228,7 +228,7 @@ export default {
|
|||
CHOICE_OPTION_TYPES: () => CHOICE_OPTION_TYPES,
|
||||
element() {
|
||||
return this.$store.getters['element/getElementById'](
|
||||
this.page,
|
||||
this.elementPage,
|
||||
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
|
||||
v-model="values.data_source_id"
|
||||
small
|
||||
:data-sources="listDataSources"
|
||||
:page="page"
|
||||
:shared-data-sources="listSharedDataSources"
|
||||
:local-data-sources="listLocalDataSources"
|
||||
>
|
||||
<template #chooseValueState>
|
||||
{{ $t('recordSelectorElementForm.noDataSourceMessage') }}
|
||||
|
@ -186,10 +186,18 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
// For now, RecordSelector only supports data sources that return arrays
|
||||
listDataSources() {
|
||||
return this.dataSources.filter(
|
||||
listLocalDataSources() {
|
||||
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.type &&
|
||||
this.$registry.get('service', dataSource.type).returnsList
|
||||
)
|
||||
},
|
||||
|
|
|
@ -10,8 +10,8 @@
|
|||
<DataSourceDropdown
|
||||
v-model="values.data_source_id"
|
||||
small
|
||||
:data-sources="dataSources"
|
||||
:page="page"
|
||||
:shared-data-sources="sharedDataSources"
|
||||
:local-data-sources="localDataSources"
|
||||
>
|
||||
<template #chooseValueState>
|
||||
{{ $t('collectionElementForm.noDataSourceMessage') }}
|
||||
|
@ -150,7 +150,6 @@
|
|||
<script>
|
||||
import _ from 'lodash'
|
||||
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 DeviceSelector from '@baserow/modules/builder/components/page/header/DeviceSelector.vue'
|
||||
import { mapActions, mapGetters } from 'vuex'
|
||||
|
@ -170,7 +169,7 @@ export default {
|
|||
InjectedFormulaInput,
|
||||
ServiceSchemaPropertySelector,
|
||||
},
|
||||
mixins: [elementForm, collectionElementForm],
|
||||
mixins: [collectionElementForm],
|
||||
inject: ['applicationContext'],
|
||||
data() {
|
||||
return {
|
||||
|
|
|
@ -17,8 +17,8 @@
|
|||
<DataSourceDropdown
|
||||
v-model="computedDataSourceId"
|
||||
small
|
||||
:data-sources="dataSources"
|
||||
:page="page"
|
||||
:shared-data-sources="sharedDataSources"
|
||||
:local-data-sources="localDataSources"
|
||||
>
|
||||
<template #chooseValueState>
|
||||
{{ $t('collectionElementForm.noDataSourceMessage') }}
|
||||
|
@ -259,7 +259,6 @@ import {
|
|||
minValue,
|
||||
maxValue,
|
||||
} from 'vuelidate/lib/validators'
|
||||
import elementForm from '@baserow/modules/builder/mixins/elementForm'
|
||||
import collectionElementForm from '@baserow/modules/builder/mixins/collectionElementForm'
|
||||
import { TABLE_ORIENTATION } from '@baserow/modules/builder/enums'
|
||||
import DeviceSelector from '@baserow/modules/builder/components/page/header/DeviceSelector.vue'
|
||||
|
@ -279,7 +278,7 @@ export default {
|
|||
DeviceSelector,
|
||||
CustomStyle,
|
||||
},
|
||||
mixins: [elementForm, collectionElementForm],
|
||||
mixins: [collectionElementForm],
|
||||
data() {
|
||||
return {
|
||||
allowedValues: [
|
||||
|
|
|
@ -60,7 +60,6 @@ export default {
|
|||
name: 'PropertyOptionForm',
|
||||
components: { BaserowTable },
|
||||
mixins: [form],
|
||||
inject: ['page'],
|
||||
props: {
|
||||
dataSource: {
|
||||
type: Object,
|
||||
|
|
|
@ -78,7 +78,7 @@ export default {
|
|||
name: 'Event',
|
||||
components: { WorkflowAction },
|
||||
mixins: [applicationContext],
|
||||
inject: ['workspace', 'builder', 'page'],
|
||||
inject: ['workspace', 'builder', 'elementPage'],
|
||||
props: {
|
||||
event: {
|
||||
type: Event,
|
||||
|
@ -128,7 +128,7 @@ export default {
|
|||
this.addingAction = true
|
||||
try {
|
||||
await this.actionCreateWorkflowAction({
|
||||
page: this.page,
|
||||
page: this.elementPage,
|
||||
workflowActionType: DEFAULT_WORKFLOW_ACTION_TYPE,
|
||||
eventType: this.event.name,
|
||||
configuration: {
|
||||
|
@ -143,7 +143,7 @@ export default {
|
|||
async deleteWorkflowAction(workflowAction) {
|
||||
try {
|
||||
await this.actionDeleteWorkflowAction({
|
||||
page: this.page,
|
||||
page: this.elementPage,
|
||||
workflowAction,
|
||||
})
|
||||
} catch (error) {
|
||||
|
@ -153,7 +153,7 @@ export default {
|
|||
async orderWorkflowActions(order) {
|
||||
try {
|
||||
await this.actionOrderWorkflowActions({
|
||||
page: this.page,
|
||||
page: this.elementPage,
|
||||
element: this.element,
|
||||
order,
|
||||
})
|
||||
|
|
|
@ -29,7 +29,7 @@ export default {
|
|||
mixins: [modal],
|
||||
provide() {
|
||||
return {
|
||||
page: null,
|
||||
currentPage: null,
|
||||
builder: this.builder,
|
||||
workspace: this.workspace,
|
||||
}
|
||||
|
|
|
@ -1,5 +1,14 @@
|
|||
<template>
|
||||
<ThemeProvider class="page">
|
||||
<PageElement
|
||||
v-for="element in headerElements"
|
||||
:key="element.id"
|
||||
:element="element"
|
||||
:mode="mode"
|
||||
:application-context-additions="{
|
||||
recordIndexPath: [],
|
||||
}"
|
||||
/>
|
||||
<PageElement
|
||||
v-for="element in elements"
|
||||
:key="element.id"
|
||||
|
@ -9,6 +18,15 @@
|
|||
recordIndexPath: [],
|
||||
}"
|
||||
/>
|
||||
<PageElement
|
||||
v-for="element in footerElements"
|
||||
:key="element.id"
|
||||
:element="element"
|
||||
:mode="mode"
|
||||
:application-context-additions="{
|
||||
recordIndexPath: [],
|
||||
}"
|
||||
/>
|
||||
</ThemeProvider>
|
||||
</template>
|
||||
|
||||
|
@ -17,16 +35,13 @@ import PageElement from '@baserow/modules/builder/components/page/PageElement'
|
|||
import ThemeProvider from '@baserow/modules/builder/components/theme/ThemeProvider'
|
||||
import { dimensionMixin } from '@baserow/modules/core/mixins/dimensions'
|
||||
import _ from 'lodash'
|
||||
import { PAGE_PLACES } from '@baserow/modules/builder/enums'
|
||||
|
||||
export default {
|
||||
components: { ThemeProvider, PageElement },
|
||||
mixins: [dimensionMixin],
|
||||
inject: ['builder', 'mode'],
|
||||
props: {
|
||||
page: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
path: {
|
||||
type: String,
|
||||
required: true,
|
||||
|
@ -39,6 +54,26 @@ export default {
|
|||
type: Array,
|
||||
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: {
|
||||
'dimensions.width': {
|
||||
|
|
|
@ -17,12 +17,14 @@
|
|||
<div class="element__inner-wrapper">
|
||||
<component
|
||||
:is="component"
|
||||
:key="element._.uid"
|
||||
:element="element"
|
||||
:children="children"
|
||||
:application-context-additions="{
|
||||
element,
|
||||
page: elementPage,
|
||||
}"
|
||||
class="element"
|
||||
v-on="$listeners"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -49,9 +51,9 @@ import { mapGetters } from 'vuex'
|
|||
export default {
|
||||
name: 'PageElement',
|
||||
mixins: [applicationContextMixin],
|
||||
inject: ['builder', 'page', 'mode'],
|
||||
inject: ['builder', 'mode', 'currentPage'],
|
||||
provide() {
|
||||
return { mode: this.elementMode }
|
||||
return { mode: this.elementMode, elementPage: this.elementPage }
|
||||
},
|
||||
props: {
|
||||
element: {
|
||||
|
@ -79,16 +81,23 @@ export default {
|
|||
this.elementMode === 'editing' ? 'editComponent' : 'component'
|
||||
return elementType[componentName]
|
||||
},
|
||||
children() {
|
||||
return this.$store.getters['element/getChildren'](this.page, this.element)
|
||||
},
|
||||
...mapGetters({
|
||||
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() {
|
||||
const elementType = this.$registry.get('element', this.element.type)
|
||||
const isInError = elementType.isInError({
|
||||
page: this.page,
|
||||
page: this.elementPage,
|
||||
element: this.element,
|
||||
builder: this.builder,
|
||||
})
|
||||
|
@ -97,6 +106,15 @@ export default {
|
|||
return false
|
||||
}
|
||||
|
||||
if (
|
||||
!this.elementType.isVisible({
|
||||
element: this.element,
|
||||
currentPage: this.currentPage,
|
||||
})
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
const isAuthenticated = this.$store.getters[
|
||||
'userSourceUser/isAuthenticated'
|
||||
](this.builder)
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
<template>
|
||||
<ThemeProvider
|
||||
<div
|
||||
class="page-preview__wrapper"
|
||||
:class="`page-preview__wrapper--${deviceType.type}`"
|
||||
@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="previewScaled"
|
||||
|
@ -12,36 +12,101 @@
|
|||
tabindex="0"
|
||||
@keydown="handleKeyDown"
|
||||
>
|
||||
<CallToAction
|
||||
v-if="!elements.length"
|
||||
class="page-preview__empty"
|
||||
icon="baserow-icon-plus"
|
||||
icon-color="neutral"
|
||||
icon-size="large"
|
||||
icon-rounded
|
||||
@click="$refs.addElementModal.show()"
|
||||
>
|
||||
{{ $t('pagePreview.emptyMessage') }}
|
||||
</CallToAction>
|
||||
<div v-else class="page">
|
||||
<ElementPreview
|
||||
v-for="(element, index) in elements"
|
||||
:key="element.id"
|
||||
is-root-element
|
||||
:element="element"
|
||||
:is-first-element="index === 0"
|
||||
:is-last-element="index === elements.length - 1"
|
||||
:is-copying="copyingElementIndex === index"
|
||||
:application-context-additions="{
|
||||
recordIndexPath: [],
|
||||
}"
|
||||
@move="moveElement($event)"
|
||||
/>
|
||||
</div>
|
||||
<ThemeProvider class="page">
|
||||
<template v-if="headerElements.length !== 0">
|
||||
<header
|
||||
class="page__header"
|
||||
:class="{
|
||||
'page__header--element-selected':
|
||||
pageSectionWithSelectedElement === 'header',
|
||||
}"
|
||||
>
|
||||
<ElementPreview
|
||||
v-for="(element, index) in headerElements"
|
||||
:key="element.id"
|
||||
:element="element"
|
||||
:is-first-element="index === 0"
|
||||
:is-copying="copyingElementIndex === index"
|
||||
:application-context-additions="{
|
||||
recordIndexPath: [],
|
||||
}"
|
||||
@move="moveElement($event)"
|
||||
/>
|
||||
</header>
|
||||
<div class="page-preview__separator">
|
||||
<span class="page-preview__separator-label">
|
||||
{{ $t('pagePreview.header') }}
|
||||
</span>
|
||||
</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>
|
||||
<AddElementModal ref="addElementModal" :page="page" />
|
||||
<AddElementModal ref="addElementModal" :page="currentPage" />
|
||||
</div>
|
||||
</ThemeProvider>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
|
@ -50,7 +115,7 @@ import { mapActions, mapGetters } from 'vuex'
|
|||
import ElementPreview from '@baserow/modules/builder/components/elements/ElementPreview'
|
||||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
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 ThemeProvider from '@baserow/modules/builder/components/theme/ThemeProvider.vue'
|
||||
|
||||
|
@ -62,7 +127,7 @@ export default {
|
|||
ElementPreview,
|
||||
PreviewNavigationBar,
|
||||
},
|
||||
inject: ['page', 'workspace'],
|
||||
inject: ['builder', 'currentPage', 'workspace'],
|
||||
data() {
|
||||
return {
|
||||
// The element that is currently being copied
|
||||
|
@ -73,7 +138,7 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
PLACEMENTS: () => PLACEMENTS,
|
||||
DIRECTIONS: () => DIRECTIONS,
|
||||
...mapGetters({
|
||||
deviceTypeSelected: 'page/getDeviceTypeSelected',
|
||||
elementSelected: 'element/getSelected',
|
||||
|
@ -81,11 +146,84 @@ export default {
|
|||
getClosestSiblingElement: 'element/getClosestSiblingElement',
|
||||
}),
|
||||
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() {
|
||||
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() {
|
||||
return this.deviceTypeSelected
|
||||
? this.$registry.get('device', this.deviceTypeSelected)
|
||||
|
@ -101,14 +239,14 @@ export default {
|
|||
return null
|
||||
}
|
||||
return this.$store.getters['element/getElementById'](
|
||||
this.page,
|
||||
this.elementSelectedPage,
|
||||
this.elementSelected.parent_element_id
|
||||
)
|
||||
},
|
||||
canCreateElement() {
|
||||
return this.$hasPermission(
|
||||
'builder.page.create_element',
|
||||
this.page,
|
||||
this.currentPage,
|
||||
this.workspace.id
|
||||
)
|
||||
},
|
||||
|
@ -156,9 +294,8 @@ export default {
|
|||
...mapActions({
|
||||
actionDuplicateElement: 'element/duplicate',
|
||||
actionDeleteElement: 'element/delete',
|
||||
actionMoveElement: 'element/moveElement',
|
||||
actionSelectElement: 'element/select',
|
||||
actionSelectNextElement: 'element/selectNextElement',
|
||||
actionMoveElement: 'element/move',
|
||||
}),
|
||||
preventScrollIfFocused(e) {
|
||||
if (this.$refs.previewScaled === document.activeElement) {
|
||||
|
@ -199,62 +336,57 @@ export default {
|
|||
previewScaled.style.width = `${currentWidth / 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) {
|
||||
return
|
||||
}
|
||||
|
||||
const elementType = this.$registry.get(
|
||||
'element',
|
||||
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)
|
||||
}
|
||||
await this.moveElement({
|
||||
element: this.elementSelected,
|
||||
direction,
|
||||
})
|
||||
},
|
||||
async selectNextElement(placement) {
|
||||
async moveSelection(direction) {
|
||||
if (!this.elementSelected?.id) {
|
||||
return
|
||||
}
|
||||
|
||||
const elementType = this.$registry.get(
|
||||
'element',
|
||||
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)
|
||||
const nextElement = this.elementsAround[direction]
|
||||
if (nextElement) {
|
||||
await this.actionSelectElement({ element: nextElement })
|
||||
}
|
||||
},
|
||||
async duplicateElement() {
|
||||
|
@ -265,7 +397,7 @@ export default {
|
|||
this.isDuplicating = true
|
||||
try {
|
||||
await this.actionDuplicateElement({
|
||||
page: this.page,
|
||||
page: this.elementSelectedPage,
|
||||
elementId: this.elementSelected.id,
|
||||
})
|
||||
} catch (error) {
|
||||
|
@ -278,12 +410,15 @@ export default {
|
|||
return
|
||||
}
|
||||
try {
|
||||
const siblingElementToSelect = this.getClosestSiblingElement(
|
||||
this.page,
|
||||
this.elementSelected
|
||||
)
|
||||
const siblingElementToSelect =
|
||||
this.elementsAround[DIRECTIONS.AFTER] ||
|
||||
this.elementsAround[DIRECTIONS.BEFORE] ||
|
||||
this.elementsAround[DIRECTIONS.LEFT] ||
|
||||
this.elementsAround[DIRECTIONS.RIGHT] ||
|
||||
this.parentOfElementSelected
|
||||
|
||||
await this.actionDeleteElement({
|
||||
page: this.page,
|
||||
page: this.elementSelectedPage,
|
||||
elementId: this.elementSelected.id,
|
||||
})
|
||||
if (siblingElementToSelect?.id) {
|
||||
|
@ -299,7 +434,10 @@ export default {
|
|||
}
|
||||
},
|
||||
selectChildElement() {
|
||||
const children = this.getChildren(this.page, this.elementSelected)
|
||||
const children = this.getChildren(
|
||||
this.elementSelectedPage,
|
||||
this.elementSelected
|
||||
)
|
||||
if (children.length) {
|
||||
this.actionSelectElement({ element: children[0] })
|
||||
}
|
||||
|
@ -310,30 +448,30 @@ export default {
|
|||
switch (e.key) {
|
||||
case 'ArrowUp':
|
||||
if (alternateAction) {
|
||||
this.moveElement(PLACEMENTS.BEFORE)
|
||||
this.moveSelectedElement(DIRECTIONS.BEFORE)
|
||||
} else {
|
||||
this.selectNextElement(PLACEMENTS.BEFORE)
|
||||
this.moveSelection(DIRECTIONS.BEFORE)
|
||||
}
|
||||
break
|
||||
case 'ArrowDown':
|
||||
if (alternateAction) {
|
||||
this.moveElement(PLACEMENTS.AFTER)
|
||||
this.moveSelectedElement(DIRECTIONS.AFTER)
|
||||
} else {
|
||||
this.selectNextElement(PLACEMENTS.AFTER)
|
||||
this.moveSelection(DIRECTIONS.AFTER)
|
||||
}
|
||||
break
|
||||
case 'ArrowLeft':
|
||||
if (alternateAction) {
|
||||
this.moveElement(PLACEMENTS.LEFT)
|
||||
this.moveSelectedElement(DIRECTIONS.LEFT)
|
||||
} else {
|
||||
this.selectNextElement(PLACEMENTS.LEFT)
|
||||
this.moveSelection(DIRECTIONS.LEFT)
|
||||
}
|
||||
break
|
||||
case 'ArrowRight':
|
||||
if (alternateAction) {
|
||||
this.moveElement(PLACEMENTS.RIGHT)
|
||||
this.moveSelectedElement(DIRECTIONS.RIGHT)
|
||||
} else {
|
||||
this.selectNextElement(PLACEMENTS.RIGHT)
|
||||
this.moveSelection(DIRECTIONS.RIGHT)
|
||||
}
|
||||
break
|
||||
case 'Backspace':
|
||||
|
|
|
@ -1,9 +1,9 @@
|
|||
<template>
|
||||
<PageTemplateContent
|
||||
v-if="!loading && workspace && page && builder"
|
||||
v-if="!loading && workspace && currentPage && builder"
|
||||
:workspace="workspace"
|
||||
:builder="builder"
|
||||
:page="page"
|
||||
:current-page="currentPage"
|
||||
:mode="mode"
|
||||
/>
|
||||
<PageSkeleton v-else />
|
||||
|
@ -32,7 +32,7 @@ export default {
|
|||
return {
|
||||
workspace: null,
|
||||
builder: null,
|
||||
page: null,
|
||||
currentPage: null,
|
||||
mode,
|
||||
loading: true,
|
||||
}
|
||||
|
@ -104,7 +104,7 @@ export default {
|
|||
)
|
||||
|
||||
this.builder = builder
|
||||
this.page = page
|
||||
this.currentPage = page
|
||||
this.workspace = builder.workspace
|
||||
} catch (e) {
|
||||
// In case of a network error we want to fail hard.
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div v-if="page" :key="page.id" class="page-template">
|
||||
<PageHeader :page="page" />
|
||||
<div v-if="currentPage" :key="currentPage.id" class="page-template">
|
||||
<PageHeader />
|
||||
<div class="layout__col-2-2 page-editor__content">
|
||||
<div :style="{ width: `calc(100% - ${panelWidth}px)` }">
|
||||
<PagePreview />
|
||||
|
@ -32,10 +32,13 @@ export default {
|
|||
return {
|
||||
workspace: this.workspace,
|
||||
builder: this.builder,
|
||||
page: this.page,
|
||||
currentPage: this.currentPage,
|
||||
mode,
|
||||
formulaComponent: ApplicationBuilderFormulaInput,
|
||||
applicationContext: { builder: this.builder, page: this.page, mode },
|
||||
applicationContext: {
|
||||
builder: this.builder,
|
||||
mode,
|
||||
},
|
||||
}
|
||||
},
|
||||
props: {
|
||||
|
@ -47,7 +50,7 @@ export default {
|
|||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
page: {
|
||||
currentPage: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
|
@ -63,7 +66,7 @@ export default {
|
|||
applicationContext() {
|
||||
return {
|
||||
builder: this.builder,
|
||||
page: this.page,
|
||||
page: this.currentPage,
|
||||
mode,
|
||||
}
|
||||
},
|
||||
|
@ -76,7 +79,9 @@ export default {
|
|||
)
|
||||
},
|
||||
dataSources() {
|
||||
return this.$store.getters['dataSource/getPageDataSources'](this.page)
|
||||
return this.$store.getters['dataSource/getPageDataSources'](
|
||||
this.currentPage
|
||||
)
|
||||
},
|
||||
dispatchContext() {
|
||||
return DataProviderType.getAllDataSourceDispatchContext(
|
||||
|
@ -96,7 +101,7 @@ export default {
|
|||
this.$store.dispatch(
|
||||
'dataSourceContent/debouncedFetchPageDataSourceContent',
|
||||
{
|
||||
page: this.page,
|
||||
page: this.currentPage,
|
||||
data: newDispatchContext,
|
||||
mode: this.mode,
|
||||
}
|
||||
|
|
|
@ -79,7 +79,7 @@ import { DEFAULT_USER_ROLE_PREFIX } from '@baserow/modules/builder/constants'
|
|||
export default {
|
||||
name: 'UserSourceUsersContext',
|
||||
mixins: [context],
|
||||
inject: ['page', 'builder'],
|
||||
inject: ['builder'],
|
||||
data() {
|
||||
return {
|
||||
state: null,
|
||||
|
|
|
@ -10,24 +10,49 @@
|
|||
ref="search"
|
||||
v-model="search"
|
||||
type="text"
|
||||
class="elements-list__search-input"
|
||||
class="elements-context__search-input"
|
||||
:placeholder="$t('elementsContext.searchPlaceholder')"
|
||||
/>
|
||||
</div>
|
||||
<ElementsList
|
||||
v-if="elementsVisible"
|
||||
:elements="rootElements"
|
||||
:filtered-search-elements="filteredSearchElements"
|
||||
@select="selectElement($event)"
|
||||
/>
|
||||
<div v-else class="context__description">
|
||||
{{ $t('elementsContext.noElements') }}
|
||||
<div class="elements-context__elements">
|
||||
<ElementsList
|
||||
v-if="headerElementsVisible"
|
||||
:elements="headerElements"
|
||||
:filtered-search-elements="filteredHeaderElements"
|
||||
@select="selectElement($event)"
|
||||
/>
|
||||
<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
|
||||
v-if="$hasPermission('builder.page.create_element', page, workspace.id)"
|
||||
class="elements-list__footer"
|
||||
v-if="
|
||||
$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
|
||||
:class="{
|
||||
'margin-top-1': !elementsVisible,
|
||||
|
@ -37,9 +62,11 @@
|
|||
</div>
|
||||
</div>
|
||||
<AddElementModal
|
||||
v-if="$hasPermission('builder.page.create_element', page, workspace.id)"
|
||||
v-if="
|
||||
$hasPermission('builder.page.create_element', currentPage, workspace.id)
|
||||
"
|
||||
ref="addElementModal"
|
||||
:page="page"
|
||||
:page="currentPage"
|
||||
@element-added="onElementAdded"
|
||||
/>
|
||||
</Context>
|
||||
|
@ -52,27 +79,110 @@ import AddElementButton from '@baserow/modules/builder/components/elements/AddEl
|
|||
import AddElementModal from '@baserow/modules/builder/components/elements/AddElementModal'
|
||||
import { mapActions } from 'vuex'
|
||||
import { isSubstringOfStrings } from '@baserow/modules/core/utils/string'
|
||||
import { PAGE_PLACES } from '@baserow/modules/builder/enums'
|
||||
|
||||
export default {
|
||||
name: 'ElementsContext',
|
||||
components: { AddElementModal, AddElementButton, ElementsList },
|
||||
mixins: [context],
|
||||
inject: ['workspace', 'page', 'builder', 'mode'],
|
||||
inject: ['workspace', 'currentPage', 'builder', 'mode'],
|
||||
data() {
|
||||
return {
|
||||
search: null,
|
||||
addingElementType: null,
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
isSearching() {
|
||||
return Boolean(this.search)
|
||||
},
|
||||
elementsVisible() {
|
||||
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)
|
||||
)
|
||||
},
|
||||
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() {
|
||||
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
|
||||
|
@ -102,34 +212,30 @@ export default {
|
|||
* - Repeat
|
||||
* - Image
|
||||
*/
|
||||
filteredSearchElements() {
|
||||
filterElements(elements, page) {
|
||||
let filteredToElementIds = []
|
||||
if (
|
||||
this.search === '' ||
|
||||
this.search === null ||
|
||||
this.search === undefined
|
||||
) {
|
||||
if (!this.search) {
|
||||
// If there's no search query, then there are no
|
||||
// elements to narrow the results down to.
|
||||
return filteredToElementIds
|
||||
}
|
||||
|
||||
// Iterate over all the root-level elements.
|
||||
this.rootElements.forEach((rootElement) => {
|
||||
elements.forEach((element) => {
|
||||
// Find this element's descendants and loop over them.
|
||||
const descendants = this.$store.getters['element/getDescendants'](
|
||||
this.page,
|
||||
rootElement
|
||||
page,
|
||||
element
|
||||
)
|
||||
descendants.forEach((descendant) => {
|
||||
// Build this descendant's corpus (for now, display name only)
|
||||
// and check if it matches the search query.
|
||||
const descendantCorpus = this.getElementCorpus(descendant)
|
||||
const descendantCorpus = this.getElementCorpus(descendant, page)
|
||||
if (isSubstringOfStrings([descendantCorpus], this.search)) {
|
||||
// The descendant matches. We need to include *this* element,
|
||||
// and all its *ancestors* in our list of narrowed results.
|
||||
const ascendants = this.$store.getters['element/getAncestors'](
|
||||
this.page,
|
||||
page,
|
||||
descendant
|
||||
)
|
||||
filteredToElementIds.push(descendant.id)
|
||||
|
@ -141,42 +247,15 @@ export default {
|
|||
|
||||
// Test of the root element itself matches the search query.
|
||||
// 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)) {
|
||||
// The root element matches.
|
||||
filteredToElementIds.push(rootElement.id)
|
||||
filteredToElementIds.push(element.id)
|
||||
}
|
||||
})
|
||||
filteredToElementIds = [...new Set(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() {
|
||||
this.search = null
|
||||
this.$nextTick(() => {
|
||||
|
|
|
@ -2,7 +2,7 @@
|
|||
<ul class="header__filter">
|
||||
<template v-for="actionType in pageActionTypes">
|
||||
<li
|
||||
v-if="actionType.isActive({ page, workspace })"
|
||||
v-if="actionType.isActive({ page: currentPage, workspace })"
|
||||
:key="actionType.getType()"
|
||||
class="header__filter-item header__filter-item--right"
|
||||
>
|
||||
|
@ -14,7 +14,7 @@
|
|||
component: $refs[`component_${actionType.type}`][0],
|
||||
button: $refs[`button_${actionType.type}`][0],
|
||||
builder: builder,
|
||||
page: page,
|
||||
page: currentPage,
|
||||
})
|
||||
"
|
||||
>
|
||||
|
@ -29,7 +29,7 @@
|
|||
:is="actionType.component"
|
||||
:ref="`component_${actionType.type}`"
|
||||
:builder="builder"
|
||||
:page="page"
|
||||
:page="currentPage"
|
||||
/>
|
||||
</li>
|
||||
</template>
|
||||
|
@ -39,7 +39,7 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'PageActions',
|
||||
inject: ['workspace', 'builder', 'page'],
|
||||
inject: ['workspace', 'builder', 'currentPage'],
|
||||
computed: {
|
||||
pageActionTypes() {
|
||||
return Object.values(this.$registry.getOrderedList('pageAction'))
|
||||
|
|
|
@ -22,12 +22,6 @@ export default {
|
|||
DeviceSelector,
|
||||
PageActions,
|
||||
},
|
||||
props: {
|
||||
page: {
|
||||
type: Object,
|
||||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({ deviceTypeSelected: 'page/getDeviceTypeSelected' }),
|
||||
deviceTypes() {
|
||||
|
|
|
@ -23,7 +23,7 @@
|
|||
:is="itemType.component"
|
||||
:ref="`component_${itemType.type}`"
|
||||
:data-item-type="itemType.type"
|
||||
:page="page"
|
||||
:page="currentPage"
|
||||
/>
|
||||
</li>
|
||||
</ul>
|
||||
|
@ -32,7 +32,7 @@
|
|||
<script>
|
||||
export default {
|
||||
name: 'PageHeaderMenuItems',
|
||||
inject: ['page'],
|
||||
inject: ['currentPage'],
|
||||
computed: {
|
||||
pageHeaderItemTypes() {
|
||||
return this.$registry.getOrderedList('pageHeaderItem')
|
||||
|
|
|
@ -8,12 +8,12 @@
|
|||
</Alert>
|
||||
<PageSettingsForm
|
||||
:builder="builder"
|
||||
:page="page"
|
||||
:default-values="page"
|
||||
:page="currentPage"
|
||||
:default-values="currentPage"
|
||||
@submitted="updatePage"
|
||||
>
|
||||
<div
|
||||
v-if="$hasPermission('builder.page.update', page, workspace.id)"
|
||||
v-if="$hasPermission('builder.page.update', currentPage, workspace.id)"
|
||||
class="actions actions--right"
|
||||
>
|
||||
<Button size="large" :loading="loading" :disabled="loading">
|
||||
|
@ -34,7 +34,7 @@ export default {
|
|||
name: 'PageSettings',
|
||||
components: { PageSettingsForm },
|
||||
mixins: [error],
|
||||
inject: ['builder', 'page', 'workspace'],
|
||||
inject: ['builder', 'currentPage', 'workspace'],
|
||||
data() {
|
||||
return {
|
||||
loading: false,
|
||||
|
@ -50,7 +50,7 @@ export default {
|
|||
try {
|
||||
await this.actionUpdatePage({
|
||||
builder: this.builder,
|
||||
page: this.page,
|
||||
page: this.currentPage,
|
||||
values: {
|
||||
name,
|
||||
path,
|
||||
|
@ -60,7 +60,7 @@ export default {
|
|||
await Promise.all(
|
||||
pathPrams.map(({ name, type }) =>
|
||||
this.$store.dispatch('pageParameter/setParameter', {
|
||||
page: this.page,
|
||||
page: this.currentPage,
|
||||
name,
|
||||
value: defaultValueForParameterType(type),
|
||||
})
|
||||
|
|
|
@ -62,8 +62,13 @@ export default {
|
|||
PageSettingsNameFormElement,
|
||||
},
|
||||
mixins: [form],
|
||||
inject: ['workspace', 'builder', 'page'],
|
||||
inject: ['workspace', 'builder'],
|
||||
props: {
|
||||
page: {
|
||||
type: Object,
|
||||
required: false,
|
||||
default: null,
|
||||
},
|
||||
isCreation: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
|
|
|
@ -76,7 +76,7 @@
|
|||
allowAllRolesExceptSelected ||
|
||||
disallowAllRolesExceptSelected
|
||||
"
|
||||
class="visibility-form__role-checkbox-container"
|
||||
class="visibility-form__role-list"
|
||||
>
|
||||
<template v-if="loadingRoles">
|
||||
<div class="loading margin-bottom-1"></div>
|
||||
|
@ -85,7 +85,7 @@
|
|||
<div
|
||||
v-for="roleName in allRoles"
|
||||
:key="roleName"
|
||||
class="visibility-form__role-checkbox-div"
|
||||
class="visibility-form__role-checkbox"
|
||||
>
|
||||
<Checkbox
|
||||
:checked="isChecked(roleName)"
|
||||
|
@ -95,14 +95,11 @@
|
|||
</Checkbox>
|
||||
</div>
|
||||
|
||||
<div class="visibility-form__role-links">
|
||||
<div class="visibility-form__actions">
|
||||
<a @click.prevent="selectAllRoles">
|
||||
{{ $t('visibilityForm.rolesSelectAll') }}
|
||||
</a>
|
||||
<a
|
||||
class="visibility-form__role-links-deselect-all"
|
||||
@click.prevent="deselectAllRoles"
|
||||
>
|
||||
<a @click.prevent="deselectAllRoles">
|
||||
{{ $t('visibilityForm.rolesDeselectAll') }}
|
||||
</a>
|
||||
</div>
|
||||
|
@ -120,12 +117,13 @@
|
|||
<script>
|
||||
import { StoreItemLookupError } from '@baserow/modules/core/errors'
|
||||
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'
|
||||
|
||||
export default {
|
||||
name: 'PageVisibilityForm',
|
||||
mixins: [visibilityForm],
|
||||
mixins: [form, visibilityForm],
|
||||
data() {
|
||||
return {
|
||||
values: {
|
||||
|
|
|
@ -4,7 +4,7 @@
|
|||
|
||||
<div>
|
||||
<PageVisibilityForm
|
||||
:default-values="page"
|
||||
:default-values="currentPage"
|
||||
@values-changed="updatePageVisibility"
|
||||
/>
|
||||
</div>
|
||||
|
@ -20,7 +20,7 @@ export default {
|
|||
name: 'PageVisibilitySettings',
|
||||
components: { PageVisibilityForm },
|
||||
mixins: [error],
|
||||
inject: ['builder', 'page', 'workspace'],
|
||||
inject: ['builder', 'currentPage', 'workspace'],
|
||||
data() {
|
||||
return {}
|
||||
},
|
||||
|
@ -31,7 +31,7 @@ export default {
|
|||
try {
|
||||
await this.actionUpdatePage({
|
||||
builder: this.builder,
|
||||
page: this.page,
|
||||
page: this.currentPage,
|
||||
values,
|
||||
})
|
||||
} catch (error) {
|
||||
|
|
|
@ -22,13 +22,8 @@ export default {
|
|||
name: 'EventsSidePanel',
|
||||
components: { Event },
|
||||
mixins: [elementSidePanel],
|
||||
inject: ['applicationContext'],
|
||||
provide() {
|
||||
return {
|
||||
applicationContext: {
|
||||
...this.applicationContext,
|
||||
element: this.element,
|
||||
},
|
||||
dataProvidersAllowed: DATA_PROVIDERS_ALLOWED_WORKFLOW_ACTIONS,
|
||||
}
|
||||
},
|
||||
|
@ -38,7 +33,7 @@ export default {
|
|||
},
|
||||
workflowActions() {
|
||||
return this.$store.getters['workflowAction/getElementWorkflowActions'](
|
||||
this.page,
|
||||
this.elementPage,
|
||||
this.element.id
|
||||
)
|
||||
},
|
||||
|
|
|
@ -16,13 +16,8 @@ import { DATA_PROVIDERS_ALLOWED_ELEMENTS } from '@baserow/modules/builder/enums'
|
|||
export default {
|
||||
name: 'GeneralSidePanel',
|
||||
mixins: [elementSidePanel],
|
||||
inject: ['applicationContext'],
|
||||
provide() {
|
||||
return {
|
||||
applicationContext: {
|
||||
...this.applicationContext,
|
||||
element: this.element,
|
||||
},
|
||||
dataProvidersAllowed: DATA_PROVIDERS_ALLOWED_ELEMENTS,
|
||||
}
|
||||
},
|
||||
|
|
|
@ -30,7 +30,6 @@ export default {
|
|||
provide() {
|
||||
return {
|
||||
builder: this.builder,
|
||||
page: null,
|
||||
mode: null,
|
||||
}
|
||||
},
|
||||
|
|
|
@ -209,7 +209,6 @@ export default {
|
|||
try {
|
||||
await this.actionUpdateUserSource({
|
||||
application: this.builder,
|
||||
page: this.page,
|
||||
userSourceId: this.editedUserSource.id,
|
||||
values: clone(newValues),
|
||||
})
|
||||
|
|
|
@ -7,14 +7,13 @@
|
|||
class="margin-bottom-2"
|
||||
>
|
||||
<div class="control__elements">
|
||||
<Dropdown v-model="values.data_source_id" :show-search="false">
|
||||
<DropdownItem
|
||||
v-for="dataSource in dataSources"
|
||||
:key="dataSource.id"
|
||||
:name="dataSource.name"
|
||||
:value="dataSource.id"
|
||||
/>
|
||||
</Dropdown>
|
||||
<DataSourceDropdown
|
||||
v-model="values.data_source_id"
|
||||
small
|
||||
:shared-data-sources="sharedDataSources"
|
||||
:local-data-sources="localDataSources"
|
||||
>
|
||||
</DataSourceDropdown>
|
||||
</div>
|
||||
</FormGroup>
|
||||
</form>
|
||||
|
@ -22,9 +21,11 @@
|
|||
|
||||
<script>
|
||||
import elementForm from '@baserow/modules/builder/mixins/elementForm'
|
||||
import DataSourceDropdown from '@baserow/modules/builder/components/dataSource/DataSourceDropdown'
|
||||
|
||||
export default {
|
||||
name: 'RefreshDataSourceWorkflowActionForm',
|
||||
components: { DataSourceDropdown },
|
||||
mixins: [elementForm],
|
||||
props: {
|
||||
workflowAction: {
|
||||
|
@ -42,8 +43,39 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
dataSources() {
|
||||
return this.$store.getters['dataSource/getPageDataSources'](this.page)
|
||||
sharedPage() {
|
||||
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,
|
||||
} from '@baserow/modules/builder/dataProviderTypes'
|
||||
|
||||
export const PLACEMENTS = {
|
||||
export const DIRECTIONS = {
|
||||
BEFORE: 'before',
|
||||
AFTER: 'after',
|
||||
LEFT: 'left',
|
||||
|
@ -83,6 +83,12 @@ export const BACKGROUND_MODES = {
|
|||
FIT: 'fit',
|
||||
}
|
||||
|
||||
export const PAGE_PLACES = {
|
||||
HEADER: 'header',
|
||||
CONTENT: 'content',
|
||||
FOOTER: 'footer',
|
||||
}
|
||||
|
||||
export const WIDTH_TYPES = {
|
||||
SMALL: { value: 'small', name: 'widthTypes.small' },
|
||||
MEDIUM: { value: 'medium', name: 'widthTypes.medium' },
|
||||
|
@ -91,6 +97,12 @@ export const WIDTH_TYPES = {
|
|||
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
|
||||
* sidebar in the application builder.
|
||||
|
|
|
@ -76,6 +76,7 @@
|
|||
},
|
||||
"elementsContext": {
|
||||
"searchPlaceholder": "Search elements",
|
||||
"noPageElements": "No elements found for this page",
|
||||
"noElements": "No elements found"
|
||||
},
|
||||
"elementType": {
|
||||
|
@ -110,7 +111,18 @@
|
|||
"recordSelector": "Record selector",
|
||||
"recordSelectorDescription": "A related record selector",
|
||||
"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": {
|
||||
"label": "Element"
|
||||
|
@ -118,7 +130,7 @@
|
|||
"addElementModal": {
|
||||
"title": "Add new element",
|
||||
"searchPlaceholder": "Search elements",
|
||||
"disabledElementTooltip": "Unavailable inside this element"
|
||||
"elementInProgress": "Adding element..."
|
||||
},
|
||||
"elementMenu": {
|
||||
"moveUp": "Move up",
|
||||
|
@ -142,7 +154,9 @@
|
|||
"message": "Click on one of the elements to see more details"
|
||||
},
|
||||
"pagePreview": {
|
||||
"emptyMessage": "Click to create first element"
|
||||
"emptyMessage": "Click to create an element",
|
||||
"header": "HEADER",
|
||||
"footer": "FOOTER"
|
||||
},
|
||||
"elementForms": {
|
||||
"textInputPlaceholder": "Enter text...",
|
||||
|
@ -375,6 +389,9 @@
|
|||
"textName": "Text",
|
||||
"numericName": "Numeric"
|
||||
},
|
||||
"pageEditor": {
|
||||
"pageNotFound": "Page not found"
|
||||
},
|
||||
"publicPage": {
|
||||
"siteNotFound": "Site not found",
|
||||
"pageNotFound": "Page not found"
|
||||
|
@ -842,7 +859,20 @@
|
|||
"dataSourceDropdown": {
|
||||
"label": "Data source",
|
||||
"noDataSources": "No data sources available",
|
||||
"noSharedDataSources": "No shared data sources available",
|
||||
"shared": "shared",
|
||||
"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) {
|
||||
return null
|
||||
}
|
||||
const pages = [this.page, this.sharedPage]
|
||||
const pages = [this.currentPage, this.sharedPage]
|
||||
return this.getPagesDataSourceById(pages, this.element.data_source_id)
|
||||
},
|
||||
dataSourceType() {
|
||||
|
@ -50,10 +50,7 @@ export default {
|
|||
return this.getHasMorePage(this.element)
|
||||
},
|
||||
contentLoading() {
|
||||
return (
|
||||
this.$fetchState.pending ||
|
||||
(this.getLoading(this.element) && !this.elementIsInError)
|
||||
)
|
||||
return this.getLoading(this.element) && !this.elementIsInError
|
||||
},
|
||||
dispatchContext() {
|
||||
return DataProviderType.getAllDataSourceDispatchContext(
|
||||
|
@ -73,7 +70,7 @@ export default {
|
|||
},
|
||||
elementIsInError() {
|
||||
return this.elementType.isInError({
|
||||
page: this.page,
|
||||
page: this.elementPage,
|
||||
element: this.element,
|
||||
builder: this.builder,
|
||||
})
|
||||
|
@ -112,7 +109,7 @@ export default {
|
|||
},
|
||||
async fetch() {
|
||||
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: {
|
||||
|
@ -134,7 +131,7 @@ export default {
|
|||
}
|
||||
try {
|
||||
await this.fetchElementContent({
|
||||
page: this.page,
|
||||
page: this.elementPage,
|
||||
element: this.element,
|
||||
dataSource: this.dataSource,
|
||||
data: this.dispatchContext,
|
||||
|
|
|
@ -1,9 +1,10 @@
|
|||
import { mapGetters } from 'vuex'
|
||||
import applicationContextMixin from '@baserow/modules/builder/mixins/applicationContext'
|
||||
import { CurrentRecordDataProviderType } from '@baserow/modules/builder/dataProviderTypes'
|
||||
import elementForm from '@baserow/modules/builder/mixins/elementForm'
|
||||
|
||||
export default {
|
||||
mixins: [applicationContextMixin],
|
||||
mixins: [elementForm, applicationContextMixin],
|
||||
computed: {
|
||||
/**
|
||||
* Returns the schema which the service schema property selector
|
||||
|
@ -38,7 +39,7 @@ export default {
|
|||
hasCollectionAncestor() {
|
||||
const { element } = this.applicationContext
|
||||
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
|
||||
|
@ -96,25 +97,47 @@ export default {
|
|||
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.
|
||||
* @returns {Array} - The data sources the page designer can choose from.
|
||||
*/
|
||||
dataSources() {
|
||||
const pages = [this.sharedPage, this.page]
|
||||
return this.$store.getters['dataSource/getPagesDataSources'](
|
||||
pages
|
||||
).filter((dataSource) => {
|
||||
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) => {
|
||||
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 =
|
||||
dataSource.type && this.$registry.get('service', dataSource.type)
|
||||
return serviceType?.getDataSchema(dataSource)
|
||||
})
|
||||
},
|
||||
dataSources() {
|
||||
return [...(this.localDataSources || []), ...this.sharedDataSources]
|
||||
},
|
||||
selectedDataSource() {
|
||||
if (!this.values.data_source_id) {
|
||||
return null
|
||||
}
|
||||
const pages = [this.sharedPage, this.page]
|
||||
const pages = [this.sharedPage, this.currentPage]
|
||||
return this.$store.getters['dataSource/getPagesDataSourceById'](
|
||||
pages,
|
||||
this.values.data_source_id
|
||||
|
|
|
@ -4,7 +4,7 @@ import { ThemeConfigBlockType } from '@baserow/modules/builder/themeConfigBlockT
|
|||
import applicationContextMixin from '@baserow/modules/builder/mixins/applicationContext'
|
||||
|
||||
export default {
|
||||
inject: ['workspace', 'builder', 'page', 'mode'],
|
||||
inject: ['workspace', 'builder', 'elementPage', 'mode'],
|
||||
mixins: [element, applicationContextMixin],
|
||||
props: {
|
||||
element: {
|
||||
|
|
|
@ -1,21 +1,20 @@
|
|||
import element from '@baserow/modules/builder/mixins/element'
|
||||
import { mapGetters } from 'vuex'
|
||||
import { PLACEMENTS } from '@baserow/modules/builder/enums'
|
||||
import { DIRECTIONS } from '@baserow/modules/builder/enums'
|
||||
|
||||
export default {
|
||||
mixins: [element],
|
||||
props: {
|
||||
children: {
|
||||
type: Array,
|
||||
required: false,
|
||||
default: () => [],
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
elementSelected: 'element/getSelected',
|
||||
}),
|
||||
PLACEMENTS: () => PLACEMENTS,
|
||||
DIRECTIONS: () => DIRECTIONS,
|
||||
children() {
|
||||
return this.$store.getters['element/getChildren'](
|
||||
this.elementPage,
|
||||
this.element
|
||||
)
|
||||
},
|
||||
elementSelectedId() {
|
||||
return this.elementSelected?.id
|
||||
},
|
||||
|
|
|
@ -5,7 +5,7 @@ import applicationContextMixin from '@baserow/modules/builder/mixins/application
|
|||
import { ThemeConfigBlockType } from '@baserow/modules/builder/themeConfigBlockTypes'
|
||||
|
||||
export default {
|
||||
inject: ['workspace', 'builder', 'page', 'mode'],
|
||||
inject: ['workspace', 'builder', 'currentPage', 'elementPage', 'mode'],
|
||||
mixins: [applicationContextMixin],
|
||||
props: {
|
||||
element: {
|
||||
|
@ -17,7 +17,7 @@ export default {
|
|||
workflowActionsInProgress() {
|
||||
const workflowActions = this.$store.getters[
|
||||
'workflowAction/getElementWorkflowActions'
|
||||
](this.page, this.element.id)
|
||||
](this.elementPage, this.element.id)
|
||||
const { recordIndexPath } = this.applicationContext
|
||||
const dispatchedById = this.elementType.uniqueElementId(
|
||||
this.element,
|
||||
|
@ -38,7 +38,7 @@ export default {
|
|||
},
|
||||
elementIsInError() {
|
||||
return this.elementType.isInError({
|
||||
page: this.page,
|
||||
page: this.elementPage,
|
||||
element: this.element,
|
||||
builder: this.builder,
|
||||
})
|
||||
|
@ -96,7 +96,7 @@ export default {
|
|||
|
||||
const workflowActions = this.$store.getters[
|
||||
'workflowAction/getElementWorkflowActions'
|
||||
](this.page, this.element.id).filter(
|
||||
](this.elementPage, this.element.id).filter(
|
||||
({ event: eventName }) => eventName === event.name
|
||||
)
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ThemeConfigBlockType } from '@baserow/modules/builder/themeConfigBlockT
|
|||
import form from '@baserow/modules/core/mixins/form'
|
||||
|
||||
export default {
|
||||
inject: ['workspace', 'builder', 'page', 'mode'],
|
||||
inject: ['workspace', 'builder', 'currentPage', 'elementPage', 'mode'],
|
||||
mixins: [form],
|
||||
computed: {
|
||||
themeConfigBlocks() {
|
||||
|
|
|
@ -5,7 +5,18 @@ import { clone } from '@baserow/modules/core/utils/object'
|
|||
import { notifyIf } from '@baserow/modules/core/utils/error'
|
||||
|
||||
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: {
|
||||
...mapGetters({
|
||||
element: 'element/getSelected',
|
||||
|
@ -20,11 +31,19 @@ export default {
|
|||
|
||||
parentElement() {
|
||||
return this.$store.getters['element/getElementById'](
|
||||
this.page,
|
||||
this.elementPage,
|
||||
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() {
|
||||
return this.element
|
||||
},
|
||||
|
@ -57,7 +76,7 @@ export default {
|
|||
if (Object.keys(differences).length > 0) {
|
||||
try {
|
||||
await this.actionDebouncedUpdateSelectedElement({
|
||||
page: this.page,
|
||||
page: this.elementPage,
|
||||
// Here we clone the values to prevent
|
||||
// "modification outside of the store" error
|
||||
values: clone(differences),
|
||||
|
|
|
@ -29,7 +29,7 @@ export default {
|
|||
},
|
||||
formElementData() {
|
||||
return this.$store.getters['formData/getElementFormEntry'](
|
||||
this.page,
|
||||
this.elementPage,
|
||||
this.uniqueElementId
|
||||
)
|
||||
},
|
||||
|
@ -38,7 +38,7 @@ export default {
|
|||
},
|
||||
formElementInvalid() {
|
||||
return this.$store.getters['formData/getElementInvalid'](
|
||||
this.page,
|
||||
this.elementPage,
|
||||
this.uniqueElementId
|
||||
)
|
||||
},
|
||||
|
@ -52,7 +52,7 @@ export default {
|
|||
},
|
||||
formElementTouched() {
|
||||
return this.$store.getters['formData/getElementTouched'](
|
||||
this.page,
|
||||
this.elementPage,
|
||||
this.uniqueElementId
|
||||
)
|
||||
},
|
||||
|
@ -62,7 +62,7 @@ export default {
|
|||
*/
|
||||
isDescendantOfFormContainer() {
|
||||
return this.$store.getters['element/getAncestors'](
|
||||
this.page,
|
||||
this.elementPage,
|
||||
this.element
|
||||
).some(({ type }) => type === FormContainerElementType.getType())
|
||||
},
|
||||
|
@ -85,7 +85,7 @@ export default {
|
|||
},
|
||||
setFormData(value) {
|
||||
return this.actionSetFormData({
|
||||
page: this.page,
|
||||
page: this.elementPage,
|
||||
uniqueElementId: this.uniqueElementId,
|
||||
payload: {
|
||||
value,
|
||||
|
@ -106,7 +106,7 @@ export default {
|
|||
*/
|
||||
onFormElementTouch() {
|
||||
this.$store.dispatch('formData/setElementTouched', {
|
||||
page: this.page,
|
||||
page: this.elementPage,
|
||||
wasTouched: true,
|
||||
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'
|
||||
|
||||
export default {
|
||||
inject: ['workspace', 'builder', 'page', 'mode'],
|
||||
inject: ['workspace', 'builder', 'currentPage', 'elementPage', 'mode'],
|
||||
mixins: [elementForm],
|
||||
provide() {
|
||||
return {
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
import UserSourceService from '@baserow/modules/core/services/userSource'
|
||||
import elementForm from '@baserow/modules/builder/mixins/elementForm'
|
||||
|
||||
import {
|
||||
DEFAULT_USER_ROLE_PREFIX,
|
||||
|
@ -12,7 +11,7 @@ import {
|
|||
} from '@baserow/modules/builder/constants'
|
||||
|
||||
export default {
|
||||
mixins: [elementForm],
|
||||
inject: ['builder'],
|
||||
data() {
|
||||
return {
|
||||
allRoles: [],
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
<template>
|
||||
<div class="page-editor">
|
||||
<PageHeader :page="page" />
|
||||
<PageHeader />
|
||||
<div class="layout__col-2-2 page-editor__content">
|
||||
<div :style="{ width: `calc(100% - ${panelWidth}px)` }">
|
||||
<PagePreview />
|
||||
|
@ -34,7 +34,7 @@ export default {
|
|||
return {
|
||||
workspace: this.workspace,
|
||||
builder: this.builder,
|
||||
page: this.page,
|
||||
currentPage: this.currentPage,
|
||||
mode,
|
||||
formulaComponent: ApplicationBuilderFormulaInput,
|
||||
applicationContext: this.applicationContext,
|
||||
|
@ -92,7 +92,7 @@ export default {
|
|||
next()
|
||||
},
|
||||
layout: 'app',
|
||||
async asyncData({ store, params, error, $registry }) {
|
||||
async asyncData({ store, params, error, $registry, app }) {
|
||||
const builderId = parseInt(params.builderId)
|
||||
const pageId = parseInt(params.pageId)
|
||||
|
||||
|
@ -115,7 +115,14 @@ export default {
|
|||
|
||||
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([
|
||||
store.dispatch('dataSource/fetch', {
|
||||
|
@ -139,14 +146,17 @@ export default {
|
|||
|
||||
data.workspace = workspace
|
||||
data.builder = builder
|
||||
data.page = page
|
||||
data.currentPage = page
|
||||
} catch (e) {
|
||||
// In case of a network error we want to fail hard.
|
||||
if (e.response === undefined && !(e instanceof StoreItemLookupError)) {
|
||||
throw e
|
||||
}
|
||||
|
||||
return error({ statusCode: 404, message: 'page not found.' })
|
||||
return error({
|
||||
statusCode: 404,
|
||||
message: app.i18n.t('pageEditor.pageNotFound'),
|
||||
})
|
||||
}
|
||||
|
||||
return data
|
||||
|
@ -155,12 +165,13 @@ export default {
|
|||
applicationContext() {
|
||||
return {
|
||||
builder: this.builder,
|
||||
page: this.page,
|
||||
mode,
|
||||
}
|
||||
},
|
||||
dataSources() {
|
||||
return this.$store.getters['dataSource/getPageDataSources'](this.page)
|
||||
return this.$store.getters['dataSource/getPageDataSources'](
|
||||
this.currentPage
|
||||
)
|
||||
},
|
||||
sharedPage() {
|
||||
return this.$store.getters['page/getSharedPage'](this.builder)
|
||||
|
@ -173,7 +184,7 @@ export default {
|
|||
dispatchContext() {
|
||||
return DataProviderType.getAllDataSourceDispatchContext(
|
||||
this.$registry.getAll('builderDataProvider'),
|
||||
this.applicationContext
|
||||
{ ...this.applicationContext, page: this.currentPage }
|
||||
)
|
||||
},
|
||||
// Separate dispatch context for application level shared data sources
|
||||
|
@ -195,7 +206,7 @@ export default {
|
|||
this.$store.dispatch(
|
||||
'dataSourceContent/debouncedFetchPageDataSourceContent',
|
||||
{
|
||||
page: this.page,
|
||||
page: this.currentPage,
|
||||
data: this.dispatchContext,
|
||||
mode: this.mode,
|
||||
}
|
||||
|
@ -227,7 +238,7 @@ export default {
|
|||
this.$store.dispatch(
|
||||
'dataSourceContent/debouncedFetchPageDataSourceContent',
|
||||
{
|
||||
page: this.page,
|
||||
page: this.currentPage,
|
||||
data: newDispatchContext,
|
||||
mode: this.mode,
|
||||
}
|
||||
|
|
|
@ -3,10 +3,10 @@
|
|||
<Toasts></Toasts>
|
||||
<PageContent
|
||||
v-if="canViewPage"
|
||||
:page="page"
|
||||
:path="path"
|
||||
:params="params"
|
||||
:elements="elements"
|
||||
:shared-elements="sharedElements"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -45,7 +45,7 @@ export default {
|
|||
return {
|
||||
workspace: this.workspace,
|
||||
builder: this.builder,
|
||||
page: this.page,
|
||||
currentPage: this.currentPage,
|
||||
mode: this.mode,
|
||||
formulaComponent: ApplicationBuilderFormulaInput,
|
||||
applicationContext: this.applicationContext,
|
||||
|
@ -111,6 +111,12 @@ export default {
|
|||
store.dispatch('dataSource/fetchPublished', {
|
||||
page: sharedPage,
|
||||
}),
|
||||
store.dispatch('element/fetchPublished', {
|
||||
page: sharedPage,
|
||||
}),
|
||||
store.dispatch('workflowAction/fetchPublished', {
|
||||
page: sharedPage,
|
||||
}),
|
||||
])
|
||||
|
||||
await DataProviderType.initOnceAll(
|
||||
|
@ -170,6 +176,13 @@ export default {
|
|||
}
|
||||
|
||||
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)
|
||||
|
||||
|
@ -217,7 +230,7 @@ export default {
|
|||
|
||||
return {
|
||||
builder,
|
||||
page,
|
||||
currentPage: page,
|
||||
path,
|
||||
params,
|
||||
mode,
|
||||
|
@ -226,7 +239,7 @@ export default {
|
|||
head() {
|
||||
return {
|
||||
titleTemplate: '',
|
||||
title: this.page.name,
|
||||
title: this.currentPage.name,
|
||||
bodyAttrs: {
|
||||
class: 'public-page',
|
||||
},
|
||||
|
@ -235,12 +248,11 @@ export default {
|
|||
},
|
||||
computed: {
|
||||
elements() {
|
||||
return this.$store.getters['element/getRootElements'](this.page)
|
||||
return this.$store.getters['element/getRootElements'](this.currentPage)
|
||||
},
|
||||
applicationContext() {
|
||||
return {
|
||||
builder: this.builder,
|
||||
page: this.page,
|
||||
pageParamsValue: this.params,
|
||||
mode: this.mode,
|
||||
}
|
||||
|
@ -253,13 +265,13 @@ export default {
|
|||
return userCanViewPage(
|
||||
this.$store.getters['userSourceUser/getUser'](this.builder),
|
||||
this.$store.getters['userSourceUser/isAuthenticated'](this.builder),
|
||||
this.page
|
||||
this.currentPage
|
||||
)
|
||||
},
|
||||
dispatchContext() {
|
||||
return DataProviderType.getAllDataSourceDispatchContext(
|
||||
this.$registry.getAll('builderDataProvider'),
|
||||
this.applicationContext
|
||||
{ ...this.applicationContext, page: this.currentPage }
|
||||
)
|
||||
},
|
||||
// Separate dispatch context for application level data sources
|
||||
|
@ -277,6 +289,9 @@ export default {
|
|||
this.sharedPage
|
||||
)
|
||||
},
|
||||
sharedElements() {
|
||||
return this.$store.getters['element/getRootElements'](this.sharedPage)
|
||||
},
|
||||
isAuthenticated() {
|
||||
return this.$store.getters['userSourceUser/isAuthenticated'](this.builder)
|
||||
},
|
||||
|
@ -307,7 +322,7 @@ export default {
|
|||
this.$store.dispatch(
|
||||
'dataSourceContent/debouncedFetchPageDataSourceContent',
|
||||
{
|
||||
page: this.page,
|
||||
page: this.currentPage,
|
||||
data: newDispatchContext,
|
||||
mode: this.mode,
|
||||
}
|
||||
|
@ -327,16 +342,27 @@ export default {
|
|||
{
|
||||
page: this.sharedPage,
|
||||
data: newDispatchContext,
|
||||
mode: this.mode,
|
||||
}
|
||||
)
|
||||
}
|
||||
},
|
||||
},
|
||||
async isAuthenticated() {
|
||||
// When the user logs in or out, we need to refetch the elements and actions
|
||||
// as they might have changed.
|
||||
this.$store.dispatch('element/fetchPublished', { page: this.page })
|
||||
this.$store.dispatch('workflowAction/fetchPublished', { page: this.page })
|
||||
// When the user login or logout, we need to refetch the elements and actions
|
||||
// as they might have changed
|
||||
await this.$store.dispatch('element/fetchPublished', {
|
||||
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.
|
||||
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