mirror of
https://gitlab.com/bramw/baserow.git
synced 2025-04-15 01:28:30 +00:00
Merge branch '3477-menu-element-improvements-1' into 'develop'
Improvements to Menu element Closes #3477 See merge request baserow/baserow!3190
This commit is contained in:
commit
8d58b2e3f1
27 changed files with 1017 additions and 205 deletions
backend
src/baserow
tests/baserow/contrib/builder
changelog/entries/unreleased/feature
web-frontend/modules
builder
core
assets/scss/components/builder/elements
plugins
|
@ -1,7 +1,5 @@
|
|||
from django.apps import AppConfig
|
||||
|
||||
from baserow.core.feature_flags import FF_MENU_ELEMENT, feature_flag_is_enabled
|
||||
|
||||
|
||||
class BuilderConfig(AppConfig):
|
||||
name = "baserow.contrib.builder"
|
||||
|
@ -210,9 +208,7 @@ class BuilderConfig(AppConfig):
|
|||
element_type_registry.register(DateTimePickerElementType())
|
||||
element_type_registry.register(HeaderElementType())
|
||||
element_type_registry.register(FooterElementType())
|
||||
|
||||
if feature_flag_is_enabled(FF_MENU_ELEMENT):
|
||||
element_type_registry.register(MenuElementType())
|
||||
element_type_registry.register(MenuElementType())
|
||||
|
||||
from .domains.domain_types import CustomDomainType, SubDomainType
|
||||
from .domains.registries import domain_type_registry
|
||||
|
@ -259,6 +255,7 @@ class BuilderConfig(AppConfig):
|
|||
ImageThemeConfigBlockType,
|
||||
InputThemeConfigBlockType,
|
||||
LinkThemeConfigBlockType,
|
||||
MenuThemeConfigBlockType,
|
||||
PageThemeConfigBlockType,
|
||||
TableThemeConfigBlockType,
|
||||
TypographyThemeConfigBlockType,
|
||||
|
@ -272,6 +269,7 @@ class BuilderConfig(AppConfig):
|
|||
theme_config_block_registry.register(PageThemeConfigBlockType())
|
||||
theme_config_block_registry.register(InputThemeConfigBlockType())
|
||||
theme_config_block_registry.register(TableThemeConfigBlockType())
|
||||
theme_config_block_registry.register(MenuThemeConfigBlockType())
|
||||
|
||||
from .workflow_actions.registries import builder_workflow_action_type_registry
|
||||
from .workflow_actions.workflow_action_types import (
|
||||
|
|
|
@ -51,6 +51,7 @@ from baserow.contrib.builder.elements.models import (
|
|||
FormContainerElement,
|
||||
HeaderElement,
|
||||
HeadingElement,
|
||||
HorizontalAlignments,
|
||||
IFrameElement,
|
||||
ImageElement,
|
||||
InputTextElement,
|
||||
|
@ -1981,14 +1982,15 @@ class MenuElementType(ElementType):
|
|||
|
||||
type = "menu"
|
||||
model_class = MenuElement
|
||||
serializer_field_names = ["orientation", "menu_items"]
|
||||
allowed_fields = ["orientation"]
|
||||
serializer_field_names = ["orientation", "alignment", "menu_items"]
|
||||
allowed_fields = ["orientation", "alignment"]
|
||||
|
||||
serializer_mixins = [NestedMenuItemsMixin]
|
||||
request_serializer_mixins = []
|
||||
|
||||
class SerializedDict(ElementDict):
|
||||
orientation: str
|
||||
alignment: str
|
||||
menu_items: List[Dict]
|
||||
|
||||
@property
|
||||
|
@ -1997,15 +1999,15 @@ class MenuElementType(ElementType):
|
|||
DynamicConfigBlockSerializer,
|
||||
)
|
||||
from baserow.contrib.builder.theme.theme_config_block_types import (
|
||||
ButtonThemeConfigBlockType,
|
||||
MenuThemeConfigBlockType,
|
||||
)
|
||||
|
||||
overrides = {
|
||||
**super().serializer_field_overrides,
|
||||
"styles": DynamicConfigBlockSerializer(
|
||||
required=False,
|
||||
property_name="button",
|
||||
theme_config_block_type_name=ButtonThemeConfigBlockType.type,
|
||||
property_name="menu",
|
||||
theme_config_block_type_name=MenuThemeConfigBlockType.type,
|
||||
serializer_kwargs={"required": False},
|
||||
),
|
||||
}
|
||||
|
@ -2138,7 +2140,10 @@ class MenuElementType(ElementType):
|
|||
super().after_update(instance, values, changes)
|
||||
|
||||
def get_pytest_params(self, pytest_data_fixture):
|
||||
return {"orientation": RepeatElement.ORIENTATIONS.VERTICAL}
|
||||
return {
|
||||
"orientation": RepeatElement.ORIENTATIONS.VERTICAL,
|
||||
"alignment": HorizontalAlignments.LEFT,
|
||||
}
|
||||
|
||||
def deserialize_property(
|
||||
self,
|
||||
|
@ -2282,3 +2287,10 @@ class MenuElementType(ElementType):
|
|||
if new_formula is not None:
|
||||
item.query_parameters[index]["value"] = new_formula
|
||||
yield item
|
||||
|
||||
for formula_field in NavigationElementManager.simple_formula_fields:
|
||||
formula = getattr(item, formula_field, "")
|
||||
new_formula = yield formula
|
||||
if new_formula is not None:
|
||||
setattr(item, formula_field, new_formula)
|
||||
yield item
|
||||
|
|
|
@ -9,6 +9,7 @@ from django.db.models import SET_NULL, QuerySet
|
|||
from baserow.contrib.builder.constants import (
|
||||
BACKGROUND_IMAGE_MODES,
|
||||
COLOR_FIELD_MAX_LENGTH,
|
||||
HorizontalAlignments,
|
||||
VerticalAlignments,
|
||||
)
|
||||
from baserow.core.constants import DATE_FORMAT_CHOICES, DATE_TIME_FORMAT_CHOICES
|
||||
|
@ -1058,4 +1059,10 @@ class MenuElement(Element):
|
|||
db_default=ORIENTATIONS.HORIZONTAL,
|
||||
)
|
||||
|
||||
alignment = models.CharField(
|
||||
choices=HorizontalAlignments.choices,
|
||||
max_length=10,
|
||||
default=HorizontalAlignments.LEFT,
|
||||
)
|
||||
|
||||
menu_items = models.ManyToManyField(MenuItemElement)
|
||||
|
|
|
@ -0,0 +1,312 @@
|
|||
# Generated by Django 5.0.9 on 2025-03-05 10:12
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.db import migrations, models
|
||||
|
||||
import baserow.core.fields
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
dependencies = [
|
||||
("builder", "0052_menuitemelement_menuelement"),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name="buttonthemeconfigblock",
|
||||
name="button_active_background_color",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
default="#4783db",
|
||||
help_text="The background color of buttons when active",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="buttonthemeconfigblock",
|
||||
name="button_active_border_color",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
default="#275d9f",
|
||||
help_text="The border color of buttons when active",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="buttonthemeconfigblock",
|
||||
name="button_active_text_color",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
default="#ffffffff",
|
||||
help_text="The text color of buttons when active",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="linkthemeconfigblock",
|
||||
name="link_active_text_color",
|
||||
field=models.CharField(
|
||||
blank=True,
|
||||
default="#275d9f",
|
||||
help_text="The hover color of links when active",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name="menuelement",
|
||||
name="alignment",
|
||||
field=models.CharField(
|
||||
choices=[("left", "Left"), ("center", "Center"), ("right", "Right")],
|
||||
default="left",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
migrations.CreateModel(
|
||||
name="MenuThemeConfigBlock",
|
||||
fields=[
|
||||
(
|
||||
"id",
|
||||
models.AutoField(
|
||||
auto_created=True,
|
||||
primary_key=True,
|
||||
serialize=False,
|
||||
verbose_name="ID",
|
||||
),
|
||||
),
|
||||
(
|
||||
"button_font_family",
|
||||
models.CharField(default="inter", max_length=250),
|
||||
),
|
||||
("button_font_size", models.SmallIntegerField(default=13)),
|
||||
(
|
||||
"button_font_weight",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("thin", "Thin"),
|
||||
("extra-light", "Extra Light"),
|
||||
("light", "Light"),
|
||||
("regular", "Regular"),
|
||||
("medium", "Medium"),
|
||||
("semi-bold", "Semi Bold"),
|
||||
("bold", "Bold"),
|
||||
("extra-bold", "Extra Bold"),
|
||||
("heavy", "Heavy"),
|
||||
("black", "Black"),
|
||||
("extra-black", "Extra Black"),
|
||||
],
|
||||
db_default="regular",
|
||||
default="regular",
|
||||
max_length=11,
|
||||
),
|
||||
),
|
||||
(
|
||||
"button_alignment",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("left", "Left"),
|
||||
("center", "Center"),
|
||||
("right", "Right"),
|
||||
],
|
||||
default="left",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
(
|
||||
"button_text_alignment",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("left", "Left"),
|
||||
("center", "Center"),
|
||||
("right", "Right"),
|
||||
],
|
||||
default="center",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
(
|
||||
"button_width",
|
||||
models.CharField(
|
||||
choices=[("auto", "Auto"), ("full", "Full")],
|
||||
default="auto",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
(
|
||||
"button_background_color",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default="primary",
|
||||
help_text="The background color of buttons",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"button_text_color",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default="#ffffffff",
|
||||
help_text="The text color of buttons",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"button_border_color",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default="border",
|
||||
help_text="The border color of buttons",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"button_border_size",
|
||||
models.SmallIntegerField(default=0, help_text="Button border size"),
|
||||
),
|
||||
(
|
||||
"button_border_radius",
|
||||
models.SmallIntegerField(
|
||||
default=4, help_text="Button border radius"
|
||||
),
|
||||
),
|
||||
(
|
||||
"button_vertical_padding",
|
||||
models.SmallIntegerField(
|
||||
default=12, help_text="Button vertical padding"
|
||||
),
|
||||
),
|
||||
(
|
||||
"button_horizontal_padding",
|
||||
models.SmallIntegerField(
|
||||
default=12, help_text="Button horizontal padding"
|
||||
),
|
||||
),
|
||||
(
|
||||
"button_hover_background_color",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default="#96baf6ff",
|
||||
help_text="The background color of buttons when hovered",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"button_hover_text_color",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default="#ffffffff",
|
||||
help_text="The text color of buttons when hovered",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"button_hover_border_color",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default="border",
|
||||
help_text="The border color of buttons when hovered",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"button_active_background_color",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default="#4783db",
|
||||
help_text="The background color of buttons when active",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"button_active_text_color",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default="#ffffffff",
|
||||
help_text="The text color of buttons when active",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"button_active_border_color",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default="#275d9f",
|
||||
help_text="The border color of buttons when active",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
("link_font_family", models.CharField(default="inter", max_length=250)),
|
||||
("link_font_size", models.SmallIntegerField(default=13)),
|
||||
(
|
||||
"link_font_weight",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("thin", "Thin"),
|
||||
("extra-light", "Extra Light"),
|
||||
("light", "Light"),
|
||||
("regular", "Regular"),
|
||||
("medium", "Medium"),
|
||||
("semi-bold", "Semi Bold"),
|
||||
("bold", "Bold"),
|
||||
("extra-bold", "Extra Bold"),
|
||||
("heavy", "Heavy"),
|
||||
("black", "Black"),
|
||||
("extra-black", "Extra Black"),
|
||||
],
|
||||
db_default="regular",
|
||||
default="regular",
|
||||
max_length=11,
|
||||
),
|
||||
),
|
||||
(
|
||||
"link_text_alignment",
|
||||
models.CharField(
|
||||
choices=[
|
||||
("left", "Left"),
|
||||
("center", "Center"),
|
||||
("right", "Right"),
|
||||
],
|
||||
default="left",
|
||||
max_length=10,
|
||||
),
|
||||
),
|
||||
(
|
||||
"link_text_color",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default="primary",
|
||||
help_text="The text color of links",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"link_hover_text_color",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default="#96baf6ff",
|
||||
help_text="The hover color of links when hovered",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"link_active_text_color",
|
||||
models.CharField(
|
||||
blank=True,
|
||||
default="#275d9f",
|
||||
help_text="The hover color of links when active",
|
||||
max_length=255,
|
||||
),
|
||||
),
|
||||
(
|
||||
"builder",
|
||||
baserow.core.fields.AutoOneToOneField(
|
||||
on_delete=django.db.models.deletion.CASCADE,
|
||||
related_name="%(class)s",
|
||||
to="builder.builder",
|
||||
),
|
||||
),
|
||||
],
|
||||
options={
|
||||
"abstract": False,
|
||||
},
|
||||
),
|
||||
]
|
|
@ -181,7 +181,7 @@ class TypographyThemeConfigBlock(ThemeConfigBlock):
|
|||
)
|
||||
|
||||
|
||||
class ButtonThemeConfigBlock(ThemeConfigBlock):
|
||||
class ButtonThemeConfigBlockMixin(models.Model):
|
||||
button_font_family = models.CharField(
|
||||
max_length=250,
|
||||
default="inter",
|
||||
|
@ -256,9 +256,34 @@ class ButtonThemeConfigBlock(ThemeConfigBlock):
|
|||
blank=True,
|
||||
help_text="The border color of buttons when hovered",
|
||||
)
|
||||
button_active_background_color = models.CharField(
|
||||
max_length=COLOR_FIELD_MAX_LENGTH,
|
||||
default="#4783db",
|
||||
blank=True,
|
||||
help_text="The background color of buttons when active",
|
||||
)
|
||||
button_active_text_color = models.CharField(
|
||||
max_length=COLOR_FIELD_MAX_LENGTH,
|
||||
default="#ffffffff",
|
||||
blank=True,
|
||||
help_text="The text color of buttons when active",
|
||||
)
|
||||
button_active_border_color = models.CharField(
|
||||
max_length=COLOR_FIELD_MAX_LENGTH,
|
||||
default="#275d9f",
|
||||
blank=True,
|
||||
help_text="The border color of buttons when active",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class LinkThemeConfigBlock(ThemeConfigBlock):
|
||||
class ButtonThemeConfigBlock(ButtonThemeConfigBlockMixin, ThemeConfigBlock):
|
||||
pass
|
||||
|
||||
|
||||
class LinkThemeConfigBlockMixin(models.Model):
|
||||
link_font_family = models.CharField(
|
||||
max_length=250,
|
||||
default="inter",
|
||||
|
@ -287,6 +312,19 @@ class LinkThemeConfigBlock(ThemeConfigBlock):
|
|||
blank=True,
|
||||
help_text="The hover color of links when hovered",
|
||||
)
|
||||
link_active_text_color = models.CharField(
|
||||
max_length=COLOR_FIELD_MAX_LENGTH,
|
||||
default="#275d9f",
|
||||
blank=True,
|
||||
help_text="The hover color of links when active",
|
||||
)
|
||||
|
||||
class Meta:
|
||||
abstract = True
|
||||
|
||||
|
||||
class LinkThemeConfigBlock(LinkThemeConfigBlockMixin, ThemeConfigBlock):
|
||||
pass
|
||||
|
||||
|
||||
class ImageThemeConfigBlock(ThemeConfigBlock):
|
||||
|
@ -535,3 +573,9 @@ class TableThemeConfigBlock(ThemeConfigBlock):
|
|||
table_horizontal_separator_size = models.SmallIntegerField(
|
||||
default=1, help_text="Table horizontal separator size"
|
||||
)
|
||||
|
||||
|
||||
class MenuThemeConfigBlock(
|
||||
LinkThemeConfigBlockMixin, ButtonThemeConfigBlockMixin, ThemeConfigBlock
|
||||
):
|
||||
pass
|
||||
|
|
|
@ -15,6 +15,7 @@ from .models import (
|
|||
ImageThemeConfigBlock,
|
||||
InputThemeConfigBlock,
|
||||
LinkThemeConfigBlock,
|
||||
MenuThemeConfigBlock,
|
||||
PageThemeConfigBlock,
|
||||
TableThemeConfigBlock,
|
||||
ThemeConfigBlock,
|
||||
|
@ -182,3 +183,8 @@ class InputThemeConfigBlockType(ThemeConfigBlockType):
|
|||
class TableThemeConfigBlockType(ThemeConfigBlockType):
|
||||
type = "table"
|
||||
model_class = TableThemeConfigBlock
|
||||
|
||||
|
||||
class MenuThemeConfigBlockType(ThemeConfigBlockType):
|
||||
type = "menu"
|
||||
model_class = MenuThemeConfigBlock
|
||||
|
|
|
@ -3,7 +3,6 @@ from django.conf import settings
|
|||
from baserow.core.exceptions import FeatureDisabledException
|
||||
|
||||
FF_DASHBOARDS = "dashboards"
|
||||
FF_MENU_ELEMENT = "menu_element"
|
||||
FF_ENABLE_ALL = "*"
|
||||
|
||||
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import uuid
|
||||
from copy import deepcopy
|
||||
|
||||
from baserow.contrib.builder.elements.models import (
|
||||
|
@ -11,6 +12,7 @@ from baserow.contrib.builder.elements.models import (
|
|||
InputTextElement,
|
||||
LinkElement,
|
||||
MenuElement,
|
||||
MenuItemElement,
|
||||
RecordSelectorElement,
|
||||
RepeatElement,
|
||||
TableElement,
|
||||
|
@ -120,6 +122,34 @@ class ElementFixtures:
|
|||
def create_builder_menu_element(self, user=None, page=None, **kwargs):
|
||||
return self.create_builder_element(MenuElement, user, page, **kwargs)
|
||||
|
||||
def create_builder_menu_element_items(
|
||||
self, user=None, page=None, menu_element=None, menu_items=None, **kwargs
|
||||
):
|
||||
if not menu_element:
|
||||
menu_element = self.create_builder_menu_element(
|
||||
user=user, page=page, **kwargs
|
||||
)
|
||||
|
||||
if not menu_items:
|
||||
menu_items = [
|
||||
{
|
||||
"variant": "link",
|
||||
"type": "link",
|
||||
"uid": uuid.uuid4(),
|
||||
"name": "Test Link",
|
||||
}
|
||||
]
|
||||
|
||||
created_items = MenuItemElement.objects.bulk_create(
|
||||
[
|
||||
MenuItemElement(**item, menu_item_order=index)
|
||||
for index, item in enumerate(menu_items)
|
||||
]
|
||||
)
|
||||
menu_element.menu_items.add(*created_items)
|
||||
|
||||
return menu_element
|
||||
|
||||
def create_builder_element(self, model_class, user=None, page=None, **kwargs):
|
||||
if user is None:
|
||||
user = self.create_user()
|
||||
|
|
|
@ -115,7 +115,7 @@ def test_create_menu_element(api_client, data_fixture):
|
|||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_can_update_a_table_element_fields(api_client, menu_element_fixture):
|
||||
def test_can_update_menu_element_items(api_client, menu_element_fixture):
|
||||
menu_element = menu_element_fixture["menu_element"]
|
||||
token = menu_element_fixture["token"]
|
||||
|
||||
|
|
|
@ -663,12 +663,16 @@ def test_builder_application_export(data_fixture):
|
|||
"button_hover_background_color": "#96baf6ff",
|
||||
"button_hover_text_color": "#ffffffff",
|
||||
"button_hover_border_color": "border",
|
||||
"button_active_background_color": "#4783db",
|
||||
"button_active_text_color": "#ffffffff",
|
||||
"button_active_border_color": "#275d9f",
|
||||
"link_font_family": "inter",
|
||||
"link_font_size": 13,
|
||||
"link_font_weight": "regular",
|
||||
"link_text_alignment": "left",
|
||||
"link_text_color": "primary",
|
||||
"link_hover_text_color": "#96baf6ff",
|
||||
"link_active_text_color": "#275d9f",
|
||||
"image_alignment": "left",
|
||||
"image_border_radius": 0,
|
||||
"image_max_width": 100,
|
||||
|
@ -1038,6 +1042,7 @@ IMPORT_REFERENCE = {
|
|||
"link_alignment": "left",
|
||||
"link_text_color": "primary",
|
||||
"link_hover_text_color": "#ccccccff",
|
||||
"link_active_text_color": "#275d9f",
|
||||
},
|
||||
"id": 999,
|
||||
"name": "Holly Sherman",
|
||||
|
|
|
@ -10,6 +10,7 @@ from baserow.contrib.builder.elements.element_types import (
|
|||
ImageElementType,
|
||||
InputTextElementType,
|
||||
LinkElementType,
|
||||
MenuElementType,
|
||||
TableElementType,
|
||||
TextElementType,
|
||||
)
|
||||
|
@ -208,3 +209,54 @@ def test_table_element_formula_generator(data_fixture, formula_generator_fixture
|
|||
"label": "get('current_record.field_111')"
|
||||
}
|
||||
assert table_element.data_source_id == data_source.id
|
||||
|
||||
|
||||
@pytest.mark.django_db
|
||||
def test_menu_element_formula_generator(data_fixture, formula_generator_fixture):
|
||||
"""
|
||||
Test the MenuElementType's formula_generator().
|
||||
|
||||
The MenuElement can have one or more MenuItemElements. The MenuItemElement
|
||||
has several formula fields but doesn't have a distinct type of its own.
|
||||
Therefore its formula_generator() is overridden and must be tested.
|
||||
"""
|
||||
|
||||
page = formula_generator_fixture["page"]
|
||||
menu_element = data_fixture.create_builder_menu_element_items(
|
||||
user=formula_generator_fixture["user"],
|
||||
page=page,
|
||||
)
|
||||
menu_item = menu_element.menu_items.first()
|
||||
menu_item.page_parameters = [
|
||||
{
|
||||
"name": "foo_param",
|
||||
"value": formula_generator_fixture["formula_1"],
|
||||
}
|
||||
]
|
||||
menu_item.query_parameters = [
|
||||
{
|
||||
"name": "bar_query",
|
||||
"value": formula_generator_fixture["formula_1"],
|
||||
},
|
||||
]
|
||||
menu_item.navigate_to_url = formula_generator_fixture["formula_1"]
|
||||
menu_item.save()
|
||||
|
||||
serialized_element = MenuElementType().export_serialized(menu_element)
|
||||
|
||||
imported_element = MenuElementType().import_serialized(
|
||||
formula_generator_fixture["page"],
|
||||
serialized_element,
|
||||
formula_generator_fixture["id_mapping"],
|
||||
)
|
||||
|
||||
imported_menu_item = imported_element.menu_items.first()
|
||||
assert (
|
||||
imported_menu_item.page_parameters[0]["value"]
|
||||
== formula_generator_fixture["formula_2"]
|
||||
)
|
||||
assert (
|
||||
imported_menu_item.query_parameters[0]["value"]
|
||||
== formula_generator_fixture["formula_2"]
|
||||
)
|
||||
assert imported_menu_item.navigate_to_url == formula_generator_fixture["formula_2"]
|
||||
|
|
|
@ -0,0 +1,8 @@
|
|||
{
|
||||
"type": "feature",
|
||||
"message": "Added the Menu element.",
|
||||
"domain": "builder",
|
||||
"issue_number": 3477,
|
||||
"bullet_points": [],
|
||||
"created_at": "2025-03-05"
|
||||
}
|
|
@ -1,10 +1,13 @@
|
|||
<template>
|
||||
<a
|
||||
:class="{
|
||||
'ab-link': variant !== 'button',
|
||||
'ab-button ab-button--medium': variant === 'button',
|
||||
'ab-button--full-width': variant === 'button' && fullWidth === true,
|
||||
}"
|
||||
:class="[
|
||||
{
|
||||
'ab-link': variant !== 'button',
|
||||
'ab-button ab-button--medium': variant === 'button',
|
||||
'ab-button--full-width': variant === 'button' && fullWidth === true,
|
||||
},
|
||||
forceActiveClass,
|
||||
]"
|
||||
:target="`_${target}`"
|
||||
:href="url"
|
||||
:rel="isExternalLink ? 'noopener noreferrer' : null"
|
||||
|
@ -36,7 +39,7 @@ export default {
|
|||
},
|
||||
},
|
||||
/**
|
||||
* @type {string} - Wheter the button should be full width.
|
||||
* @type {Boolean} - Wheter the button should be full width.
|
||||
*/
|
||||
fullWidth: {
|
||||
type: Boolean,
|
||||
|
@ -61,11 +64,22 @@ export default {
|
|||
type: String,
|
||||
required: true,
|
||||
},
|
||||
/**
|
||||
* @type {Boolean} - Whether the active class should be applied to the link.
|
||||
*/
|
||||
forceActive: {
|
||||
type: Boolean,
|
||||
required: false,
|
||||
default: false,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
isExternalLink() {
|
||||
return !this.url.startsWith('/')
|
||||
},
|
||||
forceActiveClass() {
|
||||
return this.forceActive ? `ab-${this.variant}--force-active` : ''
|
||||
},
|
||||
},
|
||||
methods: {
|
||||
handleClick(event) {
|
||||
|
|
|
@ -1,21 +1,27 @@
|
|||
<template>
|
||||
<div
|
||||
:style="getStyleOverride(element.variant)"
|
||||
:style="{
|
||||
'--alignment': menuAlignment,
|
||||
}"
|
||||
:class="[
|
||||
'menu-element__container',
|
||||
element.orientation === 'horizontal' ? 'horizontal' : 'vertical',
|
||||
element.orientation === 'horizontal'
|
||||
? 'menu-element__container--horizontal'
|
||||
: 'menu-element__container--vertical',
|
||||
]"
|
||||
>
|
||||
<div v-for="item in element.menu_items" :key="item.id">
|
||||
<template v-if="item.type === 'separator'">
|
||||
<div class="menu-element__menu-item-separator"></div>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'link' && !item.parent_menu_item">
|
||||
<div v-if="!item.children?.length">
|
||||
<div
|
||||
v-for="item in element.menu_items"
|
||||
:key="item.id"
|
||||
:class="`menu-element__menu-item-${item.type}`"
|
||||
>
|
||||
<template v-if="item.type === 'link' && !item.parent_menu_item">
|
||||
<div v-if="!item.children?.length" :style="getStyleOverride('menu')">
|
||||
<ABLink
|
||||
:variant="item.variant"
|
||||
:url="getItemUrl(item)"
|
||||
:target="getMenuItem(item).target"
|
||||
:force-active="menuItemIsActive(item)"
|
||||
>
|
||||
{{
|
||||
item.name
|
||||
|
@ -32,21 +38,27 @@
|
|||
ref="menuSubLinkContainer"
|
||||
@click="showSubMenu($event, item.id)"
|
||||
>
|
||||
<div class="menu-element__sub-link-menu--container">
|
||||
<a>{{ item.name }}</a>
|
||||
|
||||
<div class="menu-element__sub-link-menu--spacer"></div>
|
||||
|
||||
<div>
|
||||
<i
|
||||
class="menu-element__sub-link--expanded-icon"
|
||||
:class="
|
||||
isExpanded(item.id)
|
||||
? 'iconoir-nav-arrow-up'
|
||||
: 'iconoir-nav-arrow-down'
|
||||
"
|
||||
></i>
|
||||
</div>
|
||||
<div :style="getStyleOverride('menu')">
|
||||
<ABLink
|
||||
:variant="item.variant"
|
||||
url=""
|
||||
:force-active="sublinkIsActive(item)"
|
||||
>
|
||||
<div class="menu-element__sub-link-menu--container">
|
||||
{{ item.name }}
|
||||
<div class="menu-element__sub-link-menu-spacer"></div>
|
||||
<div>
|
||||
<i
|
||||
class="menu-element__sub-link--expanded-icon"
|
||||
:class="
|
||||
isExpanded(item.id)
|
||||
? 'iconoir-nav-arrow-up'
|
||||
: 'iconoir-nav-arrow-down'
|
||||
"
|
||||
></i>
|
||||
</div>
|
||||
</div>
|
||||
</ABLink>
|
||||
</div>
|
||||
|
||||
<Context
|
||||
|
@ -60,13 +72,14 @@
|
|||
v-for="child in item.children"
|
||||
:key="child.id"
|
||||
class="menu-element__sub-links"
|
||||
:style="getStyleOverride(child.variant)"
|
||||
:style="getStyleOverride('menu')"
|
||||
>
|
||||
<ABLink
|
||||
:variant="child.variant"
|
||||
:url="getItemUrl(child)"
|
||||
:target="getMenuItem(child).target"
|
||||
class="menu-element__sub-link"
|
||||
:force-active="menuItemIsActive(child)"
|
||||
>
|
||||
{{
|
||||
child.name
|
||||
|
@ -83,7 +96,10 @@
|
|||
</div>
|
||||
</template>
|
||||
<template v-else-if="item.type === 'button'">
|
||||
<ABButton @click="onButtonClick(item)">
|
||||
<ABButton
|
||||
:style="getStyleOverride('menu')"
|
||||
@click="onButtonClick(item)"
|
||||
>
|
||||
{{
|
||||
item.name
|
||||
? item.name ||
|
||||
|
@ -103,9 +119,19 @@
|
|||
</template>
|
||||
|
||||
<script>
|
||||
import { resolveApplicationRoute } from '@baserow/modules/builder/utils/routing'
|
||||
import element from '@baserow/modules/builder/mixins/element'
|
||||
import resolveElementUrl from '@baserow/modules/builder/utils/urlResolution'
|
||||
import ThemeProvider from '@baserow/modules/builder/components/theme/ThemeProvider'
|
||||
import { HORIZONTAL_ALIGNMENTS } from '@baserow/modules/builder/enums'
|
||||
|
||||
/**
|
||||
* CSS classes to force a Link variant to appear as active.
|
||||
*/
|
||||
const LINK_ACTIVE_CLASSES = {
|
||||
link: 'ab-link--force-active',
|
||||
button: 'ab-button--force-active',
|
||||
}
|
||||
|
||||
/**
|
||||
* @typedef MenuElement
|
||||
|
@ -125,6 +151,7 @@ export default {
|
|||
data() {
|
||||
return {
|
||||
expandedItems: {},
|
||||
activeItem: {},
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
|
@ -134,6 +161,42 @@ export default {
|
|||
menuElementType() {
|
||||
return this.$registry.get('element', 'menu')
|
||||
},
|
||||
menuAlignment() {
|
||||
const alignmentsCSS = {
|
||||
[HORIZONTAL_ALIGNMENTS.LEFT]: 'flex-start',
|
||||
[HORIZONTAL_ALIGNMENTS.CENTER]: 'center',
|
||||
[HORIZONTAL_ALIGNMENTS.RIGHT]: 'flex-end',
|
||||
}
|
||||
return alignmentsCSS[this.element.alignment]
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
/**
|
||||
* If the current page matches a menu item, that menu item is set as the
|
||||
* active item. This ensures that the active CSS style is applied to the
|
||||
* correct menu item.
|
||||
*/
|
||||
const found = resolveApplicationRoute(
|
||||
this.pages,
|
||||
this.$route.params.pathMatch
|
||||
)
|
||||
|
||||
if (!found?.length) return
|
||||
|
||||
const currentPageId = found[0].id
|
||||
|
||||
for (const item of this.element.menu_items) {
|
||||
if (!item.children.length && item.navigate_to_page_id === currentPageId) {
|
||||
this.activeItem = item
|
||||
break
|
||||
}
|
||||
for (const child of item.children) {
|
||||
if (child.navigate_to_page_id === currentPageId) {
|
||||
this.activeItem = child
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
methods: {
|
||||
showSubMenu(event, itemId) {
|
||||
|
@ -191,6 +254,21 @@ export default {
|
|||
this.menuElementType.getEventByName(this.element, eventName)
|
||||
)
|
||||
},
|
||||
menuItemIsActive(item) {
|
||||
return this.activeItem?.uid === item.uid
|
||||
},
|
||||
getActiveParentClass(item) {
|
||||
if (item.children?.some((child) => child.uid === this.activeItem?.uid))
|
||||
return LINK_ACTIVE_CLASSES[item.variant] || ''
|
||||
|
||||
return ''
|
||||
},
|
||||
sublinkIsActive(item) {
|
||||
if (item.children?.some((child) => child.uid === this.activeItem?.uid))
|
||||
return true
|
||||
|
||||
return false
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -1,5 +1,12 @@
|
|||
<template>
|
||||
<form @submit.prevent @keydown.enter.prevent>
|
||||
<CustomStyle
|
||||
v-model="values.styles"
|
||||
style-key="menu"
|
||||
:config-block-types="['button', 'link']"
|
||||
:theme="builder.theme"
|
||||
:extra-args="{ noAlignment: true, noWidth: true }"
|
||||
/>
|
||||
<FormGroup
|
||||
:label="$t('orientations.label')"
|
||||
small-label
|
||||
|
@ -13,9 +20,20 @@
|
|||
>
|
||||
</RadioGroup>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
v-if="values.orientation === ORIENTATIONS.HORIZONTAL"
|
||||
:label="$t('menuElementForm.alignment')"
|
||||
small-label
|
||||
required
|
||||
class="margin-bottom-2"
|
||||
>
|
||||
<HorizontalAlignmentsSelector v-model="values.alignment" />
|
||||
</FormGroup>
|
||||
|
||||
<div
|
||||
ref="menuItemAddContainer"
|
||||
class="menu-element__form--add-item-container"
|
||||
class="menu-element-form__add-item-container"
|
||||
>
|
||||
<div>
|
||||
{{ $t('menuElementForm.menuItemsLabel') }}
|
||||
|
@ -37,8 +55,11 @@
|
|||
</ButtonText>
|
||||
</div>
|
||||
</div>
|
||||
<p v-if="!values.menu_items.length">
|
||||
{{ $t('menuElementForm.noMenuItemsMessage') }}
|
||||
</p>
|
||||
<Context ref="menuItemAddContext" :hide-on-click-outside="true">
|
||||
<div class="menu-element__form--add-item-context">
|
||||
<div class="menu-element-form__add-item-context">
|
||||
<ButtonText
|
||||
v-for="(menuItemType, index) in addMenuItemTypes"
|
||||
:key="index"
|
||||
|
@ -51,30 +72,51 @@
|
|||
</ButtonText>
|
||||
</div>
|
||||
</Context>
|
||||
<div v-for="item in values.menu_items" :key="item.uid">
|
||||
<MenuElementItemForm
|
||||
:default-values="item"
|
||||
@remove-item="removeMenuItem($event)"
|
||||
@values-changed="updateMenuItem"
|
||||
></MenuElementItemForm>
|
||||
<div>
|
||||
<div class="menu-element-form__items">
|
||||
<MenuElementItemForm
|
||||
v-for="(item, index) in values.menu_items"
|
||||
:key="`${item.uid}-${index}`"
|
||||
v-sortable="{
|
||||
id: item.uid,
|
||||
update: orderRootItems,
|
||||
enabled: $hasPermission(
|
||||
'builder.page.element.update',
|
||||
element,
|
||||
workspace.id
|
||||
),
|
||||
handle: '[data-sortable-handle]',
|
||||
}"
|
||||
:default-values="item"
|
||||
@remove-item="removeMenuItem($event)"
|
||||
@values-changed="updateMenuItem"
|
||||
></MenuElementItemForm>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import elementForm from '@baserow/modules/builder/mixins/elementForm'
|
||||
import { ORIENTATIONS } from '@baserow/modules/builder/enums'
|
||||
import {
|
||||
HORIZONTAL_ALIGNMENTS,
|
||||
ORIENTATIONS,
|
||||
} from '@baserow/modules/builder/enums'
|
||||
import {
|
||||
getNextAvailableNameInSequence,
|
||||
uuid,
|
||||
} from '@baserow/modules/core/utils/string'
|
||||
import { mapGetters } from 'vuex'
|
||||
import MenuElementItemForm from '@baserow/modules/builder/components/elements/components/forms/general/MenuElementItemForm'
|
||||
import CustomStyle from '@baserow/modules/builder/components/elements/components/forms/style/CustomStyle'
|
||||
import HorizontalAlignmentsSelector from '@baserow/modules/builder/components/HorizontalAlignmentsSelector'
|
||||
|
||||
export default {
|
||||
name: 'MenuElementForm',
|
||||
components: {
|
||||
MenuElementItemForm,
|
||||
CustomStyle,
|
||||
HorizontalAlignmentsSelector,
|
||||
},
|
||||
mixins: [elementForm],
|
||||
data() {
|
||||
|
@ -83,9 +125,16 @@ export default {
|
|||
value: '',
|
||||
styles: {},
|
||||
orientation: ORIENTATIONS.VERTICAL,
|
||||
alignment: HORIZONTAL_ALIGNMENTS.LEFT,
|
||||
menu_items: [],
|
||||
},
|
||||
allowedValues: ['value', 'styles', 'menu_items', 'orientation'],
|
||||
allowedValues: [
|
||||
'value',
|
||||
'styles',
|
||||
'menu_items',
|
||||
'orientation',
|
||||
'alignment',
|
||||
],
|
||||
addMenuItemTypes: [
|
||||
{
|
||||
icon: 'iconoir-link',
|
||||
|
@ -102,6 +151,11 @@ export default {
|
|||
label: this.$t('menuElementForm.menuItemAddSeparator'),
|
||||
type: 'separator',
|
||||
},
|
||||
{
|
||||
icon: 'baserow-icon-spacer',
|
||||
label: this.$t('menuElementForm.menuItemAddSpacer'),
|
||||
type: 'spacer',
|
||||
},
|
||||
],
|
||||
}
|
||||
},
|
||||
|
@ -148,6 +202,7 @@ export default {
|
|||
type,
|
||||
uid: uuid(),
|
||||
children: [],
|
||||
parent_menu_item: null, // This is the root menu item.
|
||||
},
|
||||
]
|
||||
this.$refs.menuItemAddContext.hide()
|
||||
|
@ -173,6 +228,15 @@ export default {
|
|||
return item
|
||||
})
|
||||
},
|
||||
/**
|
||||
* Responsible for sorting the root items of this menu item.
|
||||
*/
|
||||
orderRootItems(newOrder) {
|
||||
const itemsByUid = Object.fromEntries(
|
||||
this.values.menu_items.map((item) => [item.uid, item])
|
||||
)
|
||||
this.values.menu_items = newOrder.map((uid) => itemsByUid[uid])
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
@ -2,18 +2,14 @@
|
|||
<Expandable>
|
||||
<template #header="{ toggle, expanded }">
|
||||
<div
|
||||
:class="
|
||||
isStyle
|
||||
? 'menu-element__form--expandable-item-header-outline'
|
||||
: 'menu-element__form--expandable-item-header'
|
||||
"
|
||||
class="menu-element-form__item-header"
|
||||
:class="{
|
||||
'menu-element-form__item-header--outline': isStyle,
|
||||
}"
|
||||
@click.stop="!isStyle ? toggle() : null"
|
||||
>
|
||||
<div
|
||||
class="menu-element__form--expandable-item-handle"
|
||||
data-sortable-handle
|
||||
/>
|
||||
<div class="menu-element__form--expandable-item-name">
|
||||
<div class="menu-element-form__item-handle" data-sortable-handle />
|
||||
<div class="menu-element-form__item-name">
|
||||
<template v-if="values.type === 'separator'">
|
||||
{{ $t('menuElement.separator') }}
|
||||
</template>
|
||||
|
@ -24,11 +20,12 @@
|
|||
{{ values.name }}
|
||||
</template>
|
||||
</div>
|
||||
|
||||
<template v-if="isStyle">
|
||||
<ButtonIcon
|
||||
size="small"
|
||||
icon="iconoir-bin"
|
||||
@click="removeMenuItem()"
|
||||
@click="removeMenuItem(values)"
|
||||
/>
|
||||
</template>
|
||||
<template v-else>
|
||||
|
@ -41,33 +38,58 @@
|
|||
</div>
|
||||
</template>
|
||||
<template v-if="!isStyle" #default>
|
||||
<div class="menu-element__form--expanded-item">
|
||||
<FormGroup
|
||||
small-label
|
||||
horizontal
|
||||
required
|
||||
class="margin-bottom-2"
|
||||
:label="$t('menuElementForm.menuItemLabelLabel')"
|
||||
:error="fieldHasErrors('name')"
|
||||
>
|
||||
<FormInput
|
||||
v-model="v$.values.name.$model"
|
||||
:placeholder="$t('menuElementForm.namePlaceholder')"
|
||||
<div
|
||||
class="menu-element-form__item"
|
||||
:class="{ 'menu-element-form__item-child': preventItemNesting }"
|
||||
>
|
||||
<div v-if="values.type === 'button'">
|
||||
<FormGroup
|
||||
small-label
|
||||
horizontal
|
||||
required
|
||||
class="margin-bottom-2"
|
||||
:label="$t('menuElementForm.menuItemLabelLabel')"
|
||||
:error="fieldHasErrors('name')"
|
||||
/>
|
||||
<template #error>
|
||||
{{ v$.values.name.$errors[0]?.$message }}
|
||||
</template>
|
||||
<template #after-input>
|
||||
<ButtonIcon icon="iconoir-bin" @click="removeMenuItem()" />
|
||||
</template>
|
||||
</FormGroup>
|
||||
<template v-if="values.type === 'button'">
|
||||
>
|
||||
<FormInput
|
||||
v-model="v$.values.name.$model"
|
||||
:placeholder="$t('menuElementForm.namePlaceholder')"
|
||||
:error="fieldHasErrors('name')"
|
||||
/>
|
||||
<template #error>
|
||||
{{ v$.values.name.$errors[0]?.$message }}
|
||||
</template>
|
||||
<template #after-input>
|
||||
<ButtonIcon icon="iconoir-bin" @click="removeMenuItem()" />
|
||||
</template>
|
||||
</FormGroup>
|
||||
<Alert type="info-neutral">
|
||||
<p>{{ $t('menuElementForm.eventDescription') }}</p>
|
||||
</Alert>
|
||||
</template>
|
||||
<template v-else>
|
||||
</div>
|
||||
|
||||
<div v-else>
|
||||
<FormGroup
|
||||
small-label
|
||||
horizontal
|
||||
required
|
||||
class="margin-bottom-2"
|
||||
:label="$t('menuElementForm.menuItemLabelLabel')"
|
||||
:error="fieldHasErrors('name')"
|
||||
>
|
||||
<FormInput
|
||||
v-model="v$.values.name.$model"
|
||||
:placeholder="$t('menuElementForm.namePlaceholder')"
|
||||
:error="fieldHasErrors('name')"
|
||||
/>
|
||||
<template #error>
|
||||
{{ v$.values.name.$errors[0]?.$message }}
|
||||
</template>
|
||||
<template #after-input>
|
||||
<ButtonIcon icon="iconoir-bin" @click="removeMenuItem()" />
|
||||
</template>
|
||||
</FormGroup>
|
||||
|
||||
<FormGroup
|
||||
small-label
|
||||
horizontal
|
||||
|
@ -88,22 +110,36 @@
|
|||
/>
|
||||
</Dropdown>
|
||||
</FormGroup>
|
||||
|
||||
<LinkNavigationSelectionForm
|
||||
v-if="!values.children.length"
|
||||
:default-values="defaultValues"
|
||||
@values-changed="values = { ...values, ...$event }"
|
||||
/>
|
||||
<div v-for="child in values.children" :key="child.uid">
|
||||
<div class="menu-element-item-form__children">
|
||||
<MenuElementItemForm
|
||||
v-for="(child, index) in values.children"
|
||||
:key="`${child.uid}-${index}`"
|
||||
v-sortable="{
|
||||
id: child.uid,
|
||||
update: orderChildItems,
|
||||
enabled: $hasPermission(
|
||||
'builder.page.element.update',
|
||||
element,
|
||||
workspace.id
|
||||
),
|
||||
handle: '[data-sortable-handle]',
|
||||
}"
|
||||
prevent-item-nesting
|
||||
:default-values="child"
|
||||
@remove-item="removeChildItem($event)"
|
||||
@values-changed="updateChildItem"
|
||||
></MenuElementItemForm>
|
||||
</div>
|
||||
|
||||
<div
|
||||
v-if="!preventItemNesting"
|
||||
class="menu-element__add-sub-link-container"
|
||||
class="menu-element-form__add-sub-link-container"
|
||||
>
|
||||
<ButtonText
|
||||
type="primary"
|
||||
|
@ -114,13 +150,14 @@
|
|||
{{ $t('menuElementForm.addSubLink') }}
|
||||
</ButtonText>
|
||||
</div>
|
||||
</template>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
</Expandable>
|
||||
</template>
|
||||
|
||||
<script>
|
||||
import { mapGetters } from 'vuex'
|
||||
import elementForm from '@baserow/modules/builder/mixins/elementForm'
|
||||
import LinkNavigationSelectionForm from '@baserow/modules/builder/components/elements/components/forms/general/LinkNavigationSelectionForm'
|
||||
import { useVuelidate } from '@vuelidate/core'
|
||||
|
@ -139,9 +176,9 @@ export default {
|
|||
mixins: [elementForm],
|
||||
props: {
|
||||
/**
|
||||
* Controls whether ror not this menu item can nest other menu items.
|
||||
* By default, this is allowed, but if we are already in a nested menu,
|
||||
* item we should prevent further nesting.
|
||||
* Controls whether this menu item can nest other menu items. This is
|
||||
* allowed by default. Since we only allow one level of nesting for
|
||||
* sublinks, this should be false when rendering sublinks.
|
||||
*/
|
||||
preventItemNesting: {
|
||||
type: Boolean,
|
||||
|
@ -164,9 +201,15 @@ export default {
|
|||
}
|
||||
},
|
||||
computed: {
|
||||
...mapGetters({
|
||||
getElementSelected: 'element/getSelected',
|
||||
}),
|
||||
isStyle() {
|
||||
return ['separator', 'spacer'].includes(this.values.type)
|
||||
},
|
||||
element() {
|
||||
return this.getElementSelected(this.builder)
|
||||
},
|
||||
menuItemVariants() {
|
||||
return [
|
||||
{
|
||||
|
@ -223,6 +266,15 @@ export default {
|
|||
uid: uuid(),
|
||||
})
|
||||
},
|
||||
/**
|
||||
* Responsible for sorting the child items of this menu item.
|
||||
*/
|
||||
orderChildItems(newOrder) {
|
||||
const itemsByUid = Object.fromEntries(
|
||||
this.values.children.map((item) => [item.uid, item])
|
||||
)
|
||||
this.values.children = newOrder.map((uid) => itemsByUid[uid])
|
||||
},
|
||||
},
|
||||
validations() {
|
||||
return {
|
||||
|
|
|
@ -313,6 +313,73 @@
|
|||
</ABButton>
|
||||
</template>
|
||||
</ThemeConfigBlockSection>
|
||||
<ThemeConfigBlockSection :title="$t('buttonThemeConfigBlock.activeState')">
|
||||
<template #default>
|
||||
<FormGroup
|
||||
horizontal-narrow
|
||||
small-label
|
||||
required
|
||||
class="margin-bottom-2"
|
||||
:label="$t('buttonThemeConfigBlock.backgroundColor')"
|
||||
>
|
||||
<ColorInput
|
||||
v-model="v$.values.button_active_background_color.$model"
|
||||
:color-variables="colorVariables"
|
||||
:default-value="theme?.button_active_background_color"
|
||||
small
|
||||
/>
|
||||
<template #after-input>
|
||||
<ResetButton
|
||||
v-model="v$.values.button_active_background_color.$model"
|
||||
:default-value="theme?.button_active_background_color"
|
||||
/>
|
||||
</template>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
horizontal-narrow
|
||||
small-label
|
||||
class="margin-bottom-2"
|
||||
:label="$t('buttonThemeConfigBlock.textColor')"
|
||||
>
|
||||
<ColorInput
|
||||
v-model="v$.values.button_active_text_color.$model"
|
||||
:color-variables="colorVariables"
|
||||
:default-value="theme?.button_active_text_color"
|
||||
small
|
||||
/>
|
||||
<template #after-input>
|
||||
<ResetButton
|
||||
v-model="v$.values.button_active_text_color.$model"
|
||||
:default-value="theme?.button_active_text_color"
|
||||
/>
|
||||
</template>
|
||||
</FormGroup>
|
||||
<FormGroup
|
||||
horizontal-narrow
|
||||
small-label
|
||||
class="margin-bottom-2"
|
||||
:label="$t('buttonThemeConfigBlock.borderColor')"
|
||||
>
|
||||
<ColorInput
|
||||
v-model="v$.values.button_active_border_color.$model"
|
||||
:color-variables="colorVariables"
|
||||
:default-value="theme?.button_active_border_color"
|
||||
small
|
||||
/>
|
||||
<template #after-input>
|
||||
<ResetButton
|
||||
v-model="v$.values.button_active_border_color.$model"
|
||||
:default-value="theme?.button_active_border_color"
|
||||
/>
|
||||
</template>
|
||||
</FormGroup>
|
||||
</template>
|
||||
<template #preview>
|
||||
<ABButton class="ab-button--force-active">
|
||||
{{ $t('buttonThemeConfigBlock.button') }}
|
||||
</ABButton>
|
||||
</template>
|
||||
</ThemeConfigBlockSection>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
|
@ -403,6 +470,10 @@ export default {
|
|||
this.theme?.button_hover_background_color,
|
||||
button_hover_text_color: this.theme?.button_hover_text_color,
|
||||
button_hover_border_color: this.theme?.button_hover_border_color,
|
||||
button_active_background_color:
|
||||
this.theme?.button_active_background_color,
|
||||
button_active_text_color: this.theme?.button_active_text_color,
|
||||
button_active_border_color: this.theme?.button_active_border_color,
|
||||
},
|
||||
}
|
||||
},
|
||||
|
@ -558,6 +629,9 @@ export default {
|
|||
button_hover_text_color: {},
|
||||
button_border_color: {},
|
||||
button_hover_border_color: {},
|
||||
button_active_background_color: {},
|
||||
button_active_text_color: {},
|
||||
button_active_border_color: {},
|
||||
button_font_family: {},
|
||||
button_font_weight: {},
|
||||
button_text_color: {},
|
||||
|
|
|
@ -133,6 +133,35 @@
|
|||
</ABLink>
|
||||
</template>
|
||||
</ThemeConfigBlockSection>
|
||||
<ThemeConfigBlockSection :title="$t('linkThemeConfigBlock.activeState')">
|
||||
<template #default>
|
||||
<FormGroup
|
||||
horizontal-narrow
|
||||
small-label
|
||||
required
|
||||
class="margin-bottom-2"
|
||||
:label="$t('linkThemeConfigBlock.color')"
|
||||
>
|
||||
<ColorInput
|
||||
v-model="v$.values.link_active_text_color.$model"
|
||||
:color-variables="colorVariables"
|
||||
:default-value="theme?.link_active_text_color"
|
||||
small
|
||||
/>
|
||||
<template #after-input>
|
||||
<ResetButton
|
||||
v-model="v$.values.link_active_text_color.$model"
|
||||
:default-value="theme?.link_active_text_color"
|
||||
/>
|
||||
</template>
|
||||
</FormGroup>
|
||||
</template>
|
||||
<template #preview>
|
||||
<ABLink url="" class="ab-link--force-active">
|
||||
{{ $t('linkThemeConfigBlock.link') }}
|
||||
</ABLink>
|
||||
</template>
|
||||
</ThemeConfigBlockSection>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
|
@ -193,6 +222,7 @@ export default {
|
|||
link_text_color: this.theme?.link_text_color,
|
||||
link_text_alignment: this.theme?.link_text_alignment,
|
||||
link_hover_text_color: this.theme?.link_hover_text_color,
|
||||
link_active_text_color: this.theme?.link_active_text_color,
|
||||
link_font_family: this.theme?.link_font_family,
|
||||
link_font_weight: this.theme?.link_font_weight,
|
||||
link_font_size: this.theme?.link_font_size,
|
||||
|
@ -234,6 +264,7 @@ export default {
|
|||
link_text_color: {},
|
||||
link_text_alignment: {},
|
||||
link_hover_text_color: {},
|
||||
link_active_text_color: {},
|
||||
link_font_family: {},
|
||||
link_font_weight: {},
|
||||
},
|
||||
|
|
|
@ -207,6 +207,7 @@
|
|||
"menuElementForm": {
|
||||
"menuItemsLabel": "Menu items",
|
||||
"addMenuItemLink": "Add...",
|
||||
"alignment": "Alignment",
|
||||
"menuItemDefaultName": "Page",
|
||||
"menuItemLabelLabel": "Label",
|
||||
"menuItemTypeLabel": "Type",
|
||||
|
@ -222,7 +223,8 @@
|
|||
"menuItemAddButton": "Button",
|
||||
"menuItemAddSeparator": "Separator",
|
||||
"menuItemAddSpacer": "Spacer",
|
||||
"eventDescription": "To configure actions for this button, open the Events tab of this element."
|
||||
"eventDescription": "To configure actions for this button, open the Events tab of this element.",
|
||||
"noMenuItemsMessage": "Click 'Add' to add your first menu item."
|
||||
},
|
||||
"imageElement": {
|
||||
"missingValue": "Missing alt text...",
|
||||
|
@ -566,6 +568,7 @@
|
|||
"button": "Button",
|
||||
"defaultState": "Default state",
|
||||
"hoverState": "Hover state",
|
||||
"activeState": "Active state",
|
||||
"textAlignment": "Text alignment",
|
||||
"alignment": "Alignment",
|
||||
"width": "Width",
|
||||
|
@ -583,6 +586,7 @@
|
|||
"link": "Link",
|
||||
"defaultState": "Default state",
|
||||
"hoverState": "Hover state",
|
||||
"activeState": "Active state",
|
||||
"alignment": "Alignment",
|
||||
"fontFamily": "Font",
|
||||
"size": "Font size",
|
||||
|
|
|
@ -25,7 +25,6 @@ import elementContentStore from '@baserow/modules/builder/store/elementContent'
|
|||
import themeStore from '@baserow/modules/builder/store/theme'
|
||||
import workflowActionStore from '@baserow/modules/builder/store/workflowAction'
|
||||
import formDataStore from '@baserow/modules/builder/store/formData'
|
||||
import { FF_MENU_ELEMENT } from '@baserow/modules/core/plugins/featureFlags'
|
||||
import { registerRealtimeEvents } from '@baserow/modules/builder/realtime'
|
||||
import {
|
||||
HeadingElementType,
|
||||
|
@ -229,10 +228,7 @@ export default (context) => {
|
|||
app.$registry.register('element', new DateTimePickerElementType(context))
|
||||
app.$registry.register('element', new RecordSelectorElementType(context))
|
||||
app.$registry.register('element', new RepeatElementType(context))
|
||||
|
||||
if (app.$featureFlagIsEnabled(FF_MENU_ELEMENT)) {
|
||||
app.$registry.register('element', new MenuElementType(context))
|
||||
}
|
||||
app.$registry.register('element', new MenuElementType(context))
|
||||
|
||||
app.$registry.register('device', new DesktopDeviceType(context))
|
||||
app.$registry.register('device', new TabletDeviceType(context))
|
||||
|
|
|
@ -328,6 +328,10 @@ export class ButtonThemeConfigBlockType extends ThemeConfigBlockType {
|
|||
style.addColorIfExists(theme, 'button_hover_text_color')
|
||||
style.addColorIfExists(theme, 'button_border_color')
|
||||
style.addColorIfExists(theme, 'button_hover_border_color')
|
||||
style.addColorIfExists(theme, 'button_active_background_color')
|
||||
style.addColorIfExists(theme, 'button_active_text_color')
|
||||
style.addColorIfExists(theme, 'button_active_border_color')
|
||||
|
||||
style.addIfExists(theme, 'button_width', null, (v) =>
|
||||
v === WIDTHS_NEW.FULL ? '100%' : 'auto'
|
||||
)
|
||||
|
@ -378,6 +382,7 @@ export class LinkThemeConfigBlockType extends ThemeConfigBlockType {
|
|||
})
|
||||
style.addColorIfExists(theme, 'link_text_color')
|
||||
style.addColorIfExists(theme, 'link_hover_text_color')
|
||||
style.addColorIfExists(theme, 'link_active_text_color')
|
||||
style.addIfExists(
|
||||
theme,
|
||||
'link_text_alignment',
|
||||
|
|
|
@ -25,6 +25,13 @@
|
|||
text-decoration: none;
|
||||
}
|
||||
|
||||
&:not(.loading-spinner).ab-button--force-active {
|
||||
background-color: var(--button-active-background-color, $black);
|
||||
border-color: var(--button-active-border-color, $white);
|
||||
color: var(--button-active-text-color, $white);
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
&[disabled],
|
||||
&[disabled]:hover {
|
||||
cursor: not-allowed;
|
||||
|
|
|
@ -11,4 +11,9 @@
|
|||
color: var(--link-hover-text-color, $black);
|
||||
text-decoration: var(--link-hover-text-decoration, underline);
|
||||
}
|
||||
|
||||
&--force-active {
|
||||
color: var(--link-active-text-color, $black);
|
||||
text-decoration: var(--link-active-text-decoration, underline);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -5,3 +5,4 @@
|
|||
@import 'visibility_form';
|
||||
@import 'property_option_form';
|
||||
@import 'multi_page_container_element_form';
|
||||
@import 'menu_element_form';
|
||||
|
|
|
@ -0,0 +1,84 @@
|
|||
/**
|
||||
Menu Element Form styles.
|
||||
*/
|
||||
|
||||
.menu-element-form__add-item-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.menu-element-form__add-item-context {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
%menu-element-form-expandable-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 5px 0;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-element-form__item-header {
|
||||
@extend %menu-element-form-expandable-item;
|
||||
|
||||
background-color: $palette-neutral-100;
|
||||
}
|
||||
|
||||
.menu-element-form__item-error {
|
||||
font-size: 20px;
|
||||
color: #ffbdb4;
|
||||
pointer-events: none;
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
.menu-element-form__item-name {
|
||||
flex: 1;
|
||||
padding-right: 5px;
|
||||
|
||||
@extend %ellipsis;
|
||||
}
|
||||
|
||||
.menu-element-form__item-handle {
|
||||
width: 8px;
|
||||
height: 36px;
|
||||
background-image: radial-gradient($color-neutral-200 40%, transparent 40%);
|
||||
background-size: 4px 4px;
|
||||
background-repeat: repeat;
|
||||
margin: 2px;
|
||||
margin-right: 12px;
|
||||
cursor: grab;
|
||||
visibility: hidden;
|
||||
|
||||
.menu-element-form__item-header:hover & {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-element-form__item-header--outline {
|
||||
@extend %menu-element-form-expandable-item;
|
||||
|
||||
background-color: $color-neutral-10;
|
||||
border: 1px dashed $color-neutral-200;
|
||||
}
|
||||
|
||||
.menu-element-form__item {
|
||||
margin: 5px 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menu-element-form__item-child {
|
||||
margin-right: -10px;
|
||||
width: calc(100% - 10px);
|
||||
}
|
||||
|
||||
.menu-element-form__add-sub-link-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-left: 15px;
|
||||
}
|
|
@ -1,5 +1,6 @@
|
|||
.menu-element__container {
|
||||
display: flex;
|
||||
justify-content: var(--alignment);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -10,29 +11,41 @@
|
|||
user-select: none;
|
||||
}
|
||||
|
||||
.menu-element__container.vertical {
|
||||
.menu-element__container--vertical {
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
width: 100%;
|
||||
|
||||
.menu-element__menu-item-separator {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: $palette-neutral-500;
|
||||
margin: 0 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-element__container.horizontal {
|
||||
.menu-element__menu-item-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.menu-element__sub-link-menu-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.menu-element__container--horizontal {
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.horizontal .menu-element__menu-item-separator {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background-color: $palette-neutral-500;
|
||||
}
|
||||
.menu-element__menu-item-separator {
|
||||
width: 1px;
|
||||
height: 20px;
|
||||
background-color: $palette-neutral-500;
|
||||
}
|
||||
|
||||
.vertical .menu-element__menu-item-separator {
|
||||
width: 100%;
|
||||
height: 1px;
|
||||
background-color: $palette-neutral-500;
|
||||
margin: 0 5px;
|
||||
.menu-element__sub-link-menu--expanded-icon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-element__sub-link-menu--container {
|
||||
|
@ -42,20 +55,12 @@
|
|||
gap: 5px;
|
||||
}
|
||||
|
||||
.vertical .menu-element__sub-link-menu--spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.horizontal .menu-element__sub-link-menu--expanded-icon {
|
||||
margin-left: 5px;
|
||||
}
|
||||
|
||||
.menu-element__sub-links {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
padding: 5px;
|
||||
}
|
||||
|
||||
.menu-element__sub-link {
|
||||
|
@ -68,83 +73,3 @@
|
|||
.menu-element__sub-link:hover {
|
||||
background: $palette-neutral-100;
|
||||
}
|
||||
|
||||
/**
|
||||
Menu Element Form styles.
|
||||
*/
|
||||
|
||||
.menu-element__form--add-item-container {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.menu-element__form--add-item-context {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
width: 100%;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
%menu-element-form-expandable-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
margin: 5px 0;
|
||||
user-select: none;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.menu-element__form--expandable-item-header {
|
||||
@extend %menu-element-form-expandable-item;
|
||||
|
||||
background-color: $palette-neutral-100;
|
||||
}
|
||||
|
||||
.menu-element__form--expandable-item-header-outline {
|
||||
@extend %menu-element-form-expandable-item;
|
||||
|
||||
background-color: $color-neutral-10;
|
||||
border: 1px dashed $color-neutral-200;
|
||||
}
|
||||
|
||||
.menu-element__form--expandable-item-error {
|
||||
font-size: 20px;
|
||||
color: #ffbdb4;
|
||||
pointer-events: none;
|
||||
padding-right: 3px;
|
||||
}
|
||||
|
||||
.menu-element__form--expandable-item-name {
|
||||
flex: 1;
|
||||
padding-right: 5px;
|
||||
|
||||
@extend %ellipsis;
|
||||
}
|
||||
|
||||
.menu-element__form--expandable-item-handle {
|
||||
width: 8px;
|
||||
height: 36px;
|
||||
background-image: radial-gradient($color-neutral-200 40%, transparent 40%);
|
||||
background-size: 4px 4px;
|
||||
background-repeat: repeat;
|
||||
margin: 2px;
|
||||
margin-right: 12px;
|
||||
cursor: grab;
|
||||
visibility: hidden;
|
||||
|
||||
.menu-element__form--expandable-item-header:hover & {
|
||||
visibility: visible;
|
||||
}
|
||||
}
|
||||
|
||||
.menu-element__form--expanded-item {
|
||||
margin: 5px 10px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.menu-element__add-sub-link-container {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
margin-left: 15px;
|
||||
}
|
||||
|
|
|
@ -1,7 +1,6 @@
|
|||
const FF_ENABLE_ALL = '*'
|
||||
export const FF_DASHBOARDS = 'dashboards'
|
||||
export const FF_AB_SSO = 'ab_sso'
|
||||
export const FF_MENU_ELEMENT = 'menu_element'
|
||||
|
||||
/**
|
||||
* A comma separated list of feature flags used to enable in-progress or not ready
|
||||
|
|
Loading…
Add table
Reference in a new issue